mirror of
https://github.com/oven-sh/bun
synced 2026-03-01 13:01:06 +01:00
Compare commits
15 Commits
claude/mod
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60b3cbef1 | ||
|
|
57358d3062 | ||
|
|
8557ca1b87 | ||
|
|
a07758190a | ||
|
|
2185da0931 | ||
|
|
2127a3cb6c | ||
|
|
49be23280a | ||
|
|
52c138ef15 | ||
|
|
c605abc61c | ||
|
|
5f612dd2ba | ||
|
|
674759309e | ||
|
|
23be320484 | ||
|
|
3add6b1756 | ||
|
|
7059672379 | ||
|
|
aabccfc731 |
141
.github/workflows/docker-security.yml
vendored
141
.github/workflows/docker-security.yml
vendored
@@ -1,141 +0,0 @@
|
||||
name: Docker Security Updates
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run weekly on Mondays at 2 AM UTC
|
||||
- cron: '0 2 * * 1'
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'dockerhub/**'
|
||||
- '.github/workflows/docker-security.yml'
|
||||
|
||||
jobs:
|
||||
update-base-images:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'oven-sh' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
bun-version: "latest"
|
||||
|
||||
- name: Check for base image updates
|
||||
id: check-updates
|
||||
run: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Checking for base image updates..."
|
||||
UPDATES_NEEDED=false
|
||||
UPDATE_MESSAGE=""
|
||||
|
||||
# Check Debian bookworm latest
|
||||
DEBIAN_LATEST=$(docker run --rm debian:bookworm-slim cat /etc/debian_version 2>/dev/null || echo "unknown")
|
||||
echo "Latest Debian bookworm version: $DEBIAN_LATEST"
|
||||
|
||||
# Check Alpine latest
|
||||
ALPINE_CURRENT=$(grep -oP 'alpine:\K[0-9.]+' dockerhub/alpine/Dockerfile | head -1)
|
||||
ALPINE_LATEST=$(docker run --rm alpine:latest cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
|
||||
echo "Current Alpine version in Dockerfile: $ALPINE_CURRENT"
|
||||
echo "Latest Alpine version: $ALPINE_LATEST"
|
||||
|
||||
if [ "$ALPINE_CURRENT" != "$ALPINE_LATEST" ]; then
|
||||
echo "Alpine update available: $ALPINE_CURRENT -> $ALPINE_LATEST"
|
||||
UPDATES_NEEDED=true
|
||||
UPDATE_MESSAGE="${UPDATE_MESSAGE}Alpine: $ALPINE_CURRENT -> $ALPINE_LATEST\n"
|
||||
|
||||
# Update Alpine version
|
||||
sed -i "s/alpine:${ALPINE_CURRENT}/alpine:${ALPINE_LATEST}/g" dockerhub/alpine/Dockerfile
|
||||
fi
|
||||
|
||||
# Check distroless Debian version
|
||||
DISTROLESS_CURRENT=$(grep -oP 'base-nossl-debian\K[0-9]+' dockerhub/distroless/Dockerfile | head -1)
|
||||
# Debian 12 is bookworm, check if we should update
|
||||
if [ "$DISTROLESS_CURRENT" -lt "12" ]; then
|
||||
echo "Distroless needs update from debian$DISTROLESS_CURRENT to debian12"
|
||||
UPDATES_NEEDED=true
|
||||
UPDATE_MESSAGE="${UPDATE_MESSAGE}Distroless: debian$DISTROLESS_CURRENT -> debian12\n"
|
||||
sed -i "s/base-nossl-debian${DISTROLESS_CURRENT}/base-nossl-debian12/g" dockerhub/distroless/Dockerfile
|
||||
fi
|
||||
|
||||
echo "updates_needed=$UPDATES_NEEDED" >> $GITHUB_OUTPUT
|
||||
echo "update_message<<EOF" >> $GITHUB_OUTPUT
|
||||
echo -e "$UPDATE_MESSAGE" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run vulnerability scan on current images
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
# Install trivy
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
|
||||
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
|
||||
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y trivy
|
||||
|
||||
# Scan each Dockerfile
|
||||
for dockerfile in dockerhub/*/Dockerfile; do
|
||||
dir=$(dirname "$dockerfile")
|
||||
variant=$(basename "$dir")
|
||||
echo "Scanning $variant..."
|
||||
|
||||
# Build the image locally
|
||||
docker build -t "bun-test:$variant" "$dir" --build-arg BUN_VERSION=latest || true
|
||||
|
||||
# Run trivy scan
|
||||
trivy image --severity HIGH,CRITICAL "bun-test:$variant" || true
|
||||
done
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-updates.outputs.updates_needed == 'true' && github.event_name == 'schedule'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore: update Docker base images for security"
|
||||
title: "chore: update Docker base images for security"
|
||||
body: |
|
||||
## Automated Docker Base Image Updates
|
||||
|
||||
This PR updates the base images used in our Docker containers to their latest versions for security patches.
|
||||
|
||||
### Updates:
|
||||
${{ steps.check-updates.outputs.update_message }}
|
||||
|
||||
### Security Benefits:
|
||||
- Patches latest CVEs in base OS packages
|
||||
- Updates system libraries to latest stable versions
|
||||
- Ensures compliance with security best practices
|
||||
|
||||
Please review and merge to keep our Docker images secure.
|
||||
branch: docker-base-updates
|
||||
delete-branch: true
|
||||
|
||||
scan-published-images:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'oven-sh' }}
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [latest, slim, alpine, distroless, debian]
|
||||
steps:
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: 'oven/bun:${{ matrix.variant }}'
|
||||
format: 'sarif'
|
||||
output: 'trivy-results-${{ matrix.variant }}.sarif'
|
||||
severity: 'HIGH,CRITICAL'
|
||||
|
||||
- name: Upload Trivy results to GitHub Security
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: 'trivy-results-${{ matrix.variant }}.sarif'
|
||||
category: 'trivy-${{ matrix.variant }}'
|
||||
@@ -1,194 +0,0 @@
|
||||
# Docker Image Improvements
|
||||
|
||||
This document outlines the comprehensive improvements made to Bun's Docker images to address security vulnerabilities, missing features, and outdated base images.
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
### 1. Security Updates
|
||||
|
||||
#### Base Image Updates
|
||||
- **Distroless**: Updated from `debian11` to `debian12` (bookworm) - Addresses CVE vulnerabilities reported in #22594
|
||||
- **Alpine**: Updated from `3.20` to `3.21` - Latest stable Alpine release with security patches
|
||||
- **Debian/Debian-slim**: Ensured using `bookworm` (Debian 12) consistently
|
||||
|
||||
#### Automated Security Workflow
|
||||
- Added `.github/workflows/docker-security.yml` for:
|
||||
- Weekly automated base image updates
|
||||
- Vulnerability scanning with Trivy
|
||||
- Automatic PR creation for security updates
|
||||
- SARIF upload to GitHub Security tab
|
||||
|
||||
### 2. Feature Additions
|
||||
|
||||
#### Essential Packages
|
||||
Added commonly requested packages to all non-distroless images:
|
||||
- **git**: Addresses #4687 - Required for git-based npm dependencies
|
||||
- **ca-certificates**: Ensures HTTPS connections work properly
|
||||
- **python3** (debian only): For node-gyp compatibility
|
||||
|
||||
### 3. Build and Publishing Improvements
|
||||
|
||||
#### New Scripts
|
||||
- **`test-all-images.sh`**: Comprehensive testing script that:
|
||||
- Builds all Docker variants
|
||||
- Tests bun/bunx commands
|
||||
- Verifies JavaScript execution
|
||||
- Checks package installation
|
||||
- Runs security scans
|
||||
|
||||
- **`publish-images.sh`**: Production-ready publishing script that:
|
||||
- Supports multi-architecture builds (amd64/arm64)
|
||||
- Handles semantic versioning tags
|
||||
- Provides dry-run capability
|
||||
- Manages all variant suffixes correctly
|
||||
|
||||
### 4. Issues Addressed
|
||||
|
||||
The following issues are resolved or improved by these changes:
|
||||
|
||||
- **#22594**: HIGH vulnerabilities in Debian images - Fixed by updating base images
|
||||
- **#20414**: Distroless images outdated - Fixed by ensuring distroless is built in CI
|
||||
- **#4687**: Missing git in Docker images - Fixed by adding git to all images
|
||||
- **#3272**: CVEs in Docker images - Addressed via base image updates and security scanning
|
||||
- **#17463**: Shell commands issues in distroless - Documented as expected behavior
|
||||
- **#18325**: bunx not available in Alpine - Verified symlink creation
|
||||
|
||||
## Docker Image Variants
|
||||
|
||||
### 1. Debian (Full)
|
||||
- Base: `debian:bookworm`
|
||||
- Includes: git, curl, python3, ca-certificates
|
||||
- Use case: Full compatibility, development environments
|
||||
|
||||
### 2. Debian Slim
|
||||
- Base: `debian:bookworm-slim`
|
||||
- Includes: git, ca-certificates
|
||||
- Use case: Production with minimal overhead
|
||||
|
||||
### 3. Alpine
|
||||
- Base: `alpine:3.21`
|
||||
- Includes: git, ca-certificates, libgcc, libstdc++
|
||||
- Use case: Smallest image size with package management
|
||||
|
||||
### 4. Distroless
|
||||
- Base: `gcr.io/distroless/base-nossl-debian12`
|
||||
- Includes: Only Bun binary
|
||||
- Use case: Maximum security, minimal attack surface
|
||||
- Note: No shell, no package manager - purely for running Bun applications
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
```dockerfile
|
||||
# Development - full features
|
||||
FROM oven/bun:latest
|
||||
|
||||
# Production - smaller size
|
||||
FROM oven/bun:slim
|
||||
|
||||
# Minimal - smallest size with package manager
|
||||
FROM oven/bun:alpine
|
||||
|
||||
# Maximum security - no shell
|
||||
FROM oven/bun:distroless
|
||||
```
|
||||
|
||||
### With Git Dependencies
|
||||
```dockerfile
|
||||
FROM oven/bun:slim
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lockb ./
|
||||
# Git is now available for git-based dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
CMD ["bun", "run", "start"]
|
||||
```
|
||||
|
||||
### Multi-stage Build
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM oven/bun:latest AS build
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lockb ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
# Production stage - distroless for security
|
||||
FROM oven/bun:distroless
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
ENTRYPOINT ["bun", "dist/server.js"]
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
The release workflow now:
|
||||
1. Builds all variants including distroless
|
||||
2. Pushes to Docker Hub with proper tags
|
||||
3. Runs security scans
|
||||
4. Updates base images automatically
|
||||
|
||||
### Local Testing
|
||||
```bash
|
||||
# Test all images
|
||||
cd dockerhub
|
||||
./test-all-images.sh
|
||||
|
||||
# Test with specific version
|
||||
./test-all-images.sh v1.2.22
|
||||
|
||||
# Build and publish (dry run)
|
||||
./publish-images.sh
|
||||
|
||||
# Build and publish (actual push)
|
||||
PUSH_IMAGES=true BUN_VERSION=v1.2.22 ./publish-images.sh
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use distroless for production** when possible - smallest attack surface
|
||||
2. **Regular updates** - Automated weekly base image updates via GitHub Actions
|
||||
3. **Vulnerability scanning** - Integrated Trivy scanning in CI
|
||||
4. **Minimal packages** - Only essential packages included
|
||||
5. **Non-root user** - All images run as `bun` user (UID 1000)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Old Images
|
||||
If you're using older Bun Docker images:
|
||||
|
||||
1. **Update base image tags** in your Dockerfiles
|
||||
2. **Remove git installation** steps - git is now included
|
||||
3. **Check for CVE warnings** - New images address known vulnerabilities
|
||||
4. **Test distroless** - Consider migrating to distroless for production
|
||||
|
||||
### Breaking Changes
|
||||
- Distroless now uses Debian 12 instead of Debian 11
|
||||
- Alpine updated to 3.21 (check for Alpine-specific compatibility)
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Potential future enhancements:
|
||||
- [ ] Add HEALTHCHECK instructions
|
||||
- [ ] Create debug variants with additional tools
|
||||
- [ ] Add Windows container support
|
||||
- [ ] Implement image signing with cosign
|
||||
- [ ] Add SBOM (Software Bill of Materials) generation
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to Docker image improvements:
|
||||
1. Test changes with `./test-all-images.sh`
|
||||
2. Update this documentation
|
||||
3. Ensure CI passes
|
||||
4. Submit PR with clear description of changes
|
||||
|
||||
## Support
|
||||
|
||||
For Docker-related issues:
|
||||
- File issues with the `docker` label
|
||||
- Include Docker variant and version
|
||||
- Provide minimal reproduction Dockerfile
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21 AS build
|
||||
FROM alpine:3.20 AS build
|
||||
|
||||
# https://github.com/oven-sh/bun/releases
|
||||
ARG BUN_VERSION=latest
|
||||
@@ -44,7 +44,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \
|
||||
&& rm -f "bun-linux-$build.zip" SHASUMS256.txt.asc SHASUMS256.txt \
|
||||
&& chmod +x /usr/local/bin/bun
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.20
|
||||
|
||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||
# On ephemeral containers, the cache is not useful
|
||||
@@ -65,7 +65,7 @@ RUN --mount=type=bind,from=build,source=/tmp,target=/tmp \
|
||||
addgroup -g 1000 bun \
|
||||
&& adduser -u 1000 -G bun -s /bin/sh -D bun \
|
||||
&& ln -s /usr/local/bin/bun /usr/local/bin/bunx \
|
||||
&& apk add --no-cache libgcc libstdc++ git ca-certificates \
|
||||
&& apk add libgcc libstdc++ \
|
||||
&& which bun \
|
||||
&& which bunx \
|
||||
&& bun --version
|
||||
|
||||
@@ -57,14 +57,6 @@ RUN apt-get update -qq \
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install essential packages including git
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -qq --no-install-recommends \
|
||||
ca-certificates \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||
# On ephemeral containers, the cache is not useful
|
||||
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||
|
||||
@@ -58,16 +58,6 @@ RUN apt-get update -qq \
|
||||
|
||||
FROM debian:bookworm
|
||||
|
||||
# Install commonly needed packages
|
||||
RUN apt-get update -qq
|
||||
&& apt-get install -qq --no-install-recommends
|
||||
ca-certificates
|
||||
git
|
||||
curl
|
||||
python3
|
||||
&& apt-get clean
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin
|
||||
COPY --from=build /usr/local/bin/bun /usr/local/bin/bun
|
||||
RUN mkdir -p /usr/local/bun-node-fallback-bin && ln -s /usr/local/bin/bun /usr/local/bun-node-fallback-bin/node
|
||||
|
||||
@@ -55,7 +55,7 @@ RUN apt-get update -qq \
|
||||
&& which bun \
|
||||
&& bun --version
|
||||
|
||||
FROM gcr.io/distroless/base-nossl-debian12
|
||||
FROM gcr.io/distroless/base-nossl-debian11
|
||||
|
||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||
# On ephemeral containers, the cache is not useful
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# This script publishes Bun Docker images to Docker Hub
|
||||
# It should be run from CI/CD or manually with proper credentials
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DOCKER_REPO="oven/bun"
|
||||
BUN_VERSION=${BUN_VERSION:-latest}
|
||||
PUSH_IMAGES=${PUSH_IMAGES:-false}
|
||||
PLATFORMS="linux/amd64,linux/arm64"
|
||||
|
||||
echo -e "${BLUE}Bun Docker Image Publisher${NC}"
|
||||
echo "================================"
|
||||
echo "Version: $BUN_VERSION"
|
||||
echo "Repository: $DOCKER_REPO"
|
||||
echo "Push enabled: $PUSH_IMAGES"
|
||||
echo "Platforms: $PLATFORMS"
|
||||
echo ""
|
||||
|
||||
# Check if docker buildx is available
|
||||
if ! docker buildx version &> /dev/null; then
|
||||
echo -e "${RED}Error: Docker buildx is required but not found${NC}"
|
||||
echo "Please install Docker buildx: https://docs.docker.com/buildx/working-with-buildx/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Setup buildx builder
|
||||
BUILDER_NAME="bun-multiarch-builder"
|
||||
if ! docker buildx ls | grep -q "$BUILDER_NAME"; then
|
||||
echo "Creating buildx builder..."
|
||||
docker buildx create --name "$BUILDER_NAME" --platform "$PLATFORMS" --use
|
||||
else
|
||||
echo "Using existing buildx builder..."
|
||||
docker buildx use "$BUILDER_NAME"
|
||||
fi
|
||||
|
||||
# Ensure builder is running
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
# Function to build and optionally push an image
|
||||
build_and_push() {
|
||||
local variant=$1
|
||||
local dir=$2
|
||||
local tags=$3
|
||||
|
||||
echo -e "\n${YELLOW}Building $variant...${NC}"
|
||||
echo "Directory: $dir"
|
||||
echo "Tags: $tags"
|
||||
|
||||
# Prepare build arguments
|
||||
local build_args="--platform $PLATFORMS"
|
||||
build_args="$build_args --build-arg BUN_VERSION=$BUN_VERSION"
|
||||
|
||||
# Add tags
|
||||
for tag in $tags; do
|
||||
build_args="$build_args --tag $DOCKER_REPO:$tag"
|
||||
done
|
||||
|
||||
# Add push flag if enabled
|
||||
if [ "$PUSH_IMAGES" = "true" ]; then
|
||||
build_args="$build_args --push"
|
||||
else
|
||||
build_args="$build_args --load"
|
||||
fi
|
||||
|
||||
# Build the image
|
||||
echo "Running: docker buildx build $build_args $dir"
|
||||
if docker buildx build $build_args "$dir"; then
|
||||
echo -e "${GREEN}✓ Successfully built $variant${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build $variant${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Determine version tags
|
||||
determine_tags() {
|
||||
local variant=$1
|
||||
local suffix=$2
|
||||
local tags=""
|
||||
|
||||
case "$BUN_VERSION" in
|
||||
latest)
|
||||
# For latest, we tag with 'latest' and the variant name
|
||||
if [ -z "$suffix" ]; then
|
||||
tags="latest"
|
||||
else
|
||||
tags="latest$suffix"
|
||||
fi
|
||||
# Also tag with just the variant name for convenience
|
||||
tags="$tags $variant"
|
||||
;;
|
||||
canary)
|
||||
tags="canary$suffix"
|
||||
if [ "$variant" != "debian" ]; then
|
||||
tags="$tags canary-$variant"
|
||||
fi
|
||||
;;
|
||||
v*.*.*)
|
||||
# Semantic version
|
||||
local version=${BUN_VERSION#v}
|
||||
local major=$(echo "$version" | cut -d. -f1)
|
||||
local minor=$(echo "$version" | cut -d. -f2)
|
||||
|
||||
# Add full version tag
|
||||
tags="$version$suffix"
|
||||
|
||||
# Add major.minor tag
|
||||
tags="$tags $major.$minor$suffix"
|
||||
|
||||
# Add major tag
|
||||
tags="$tags $major$suffix"
|
||||
|
||||
# For non-debian variants, also add variant-specific tags
|
||||
if [ "$variant" != "debian" ]; then
|
||||
tags="$tags $version-$variant"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Custom tag
|
||||
tags="${BUN_VERSION}$suffix"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$tags"
|
||||
}
|
||||
|
||||
# Build configurations
|
||||
declare -A VARIANTS=(
|
||||
["debian"]=""
|
||||
["debian-slim"]="-slim"
|
||||
["alpine"]="-alpine"
|
||||
["distroless"]="-distroless"
|
||||
)
|
||||
|
||||
# Track results
|
||||
FAILED_BUILDS=()
|
||||
SUCCESSFUL_BUILDS=()
|
||||
|
||||
# Build each variant
|
||||
for variant in "${!VARIANTS[@]}"; do
|
||||
suffix="${VARIANTS[$variant]}"
|
||||
|
||||
# Determine directory
|
||||
if [ "$variant" = "debian-slim" ]; then
|
||||
dir="./debian-slim"
|
||||
elif [ "$variant" = "debian" ]; then
|
||||
dir="./debian"
|
||||
else
|
||||
dir="./$variant"
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo -e "${RED}✗ Directory $dir not found for $variant${NC}"
|
||||
FAILED_BUILDS+=("$variant")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Determine tags for this variant
|
||||
tags=$(determine_tags "$variant" "$suffix")
|
||||
|
||||
# Build and optionally push
|
||||
if build_and_push "$variant" "$dir" "$tags"; then
|
||||
SUCCESSFUL_BUILDS+=("$variant")
|
||||
else
|
||||
FAILED_BUILDS+=("$variant")
|
||||
fi
|
||||
done
|
||||
|
||||
# Print summary
|
||||
echo -e "\n${YELLOW}========== BUILD SUMMARY ==========${NC}"
|
||||
echo -e "${GREEN}Successful: ${#SUCCESSFUL_BUILDS[@]} variants${NC}"
|
||||
for variant in "${SUCCESSFUL_BUILDS[@]}"; do
|
||||
echo -e " ${GREEN}✓${NC} $variant"
|
||||
done
|
||||
|
||||
if [ ${#FAILED_BUILDS[@]} -gt 0 ]; then
|
||||
echo -e "${RED}Failed: ${#FAILED_BUILDS[@]} variants${NC}"
|
||||
for variant in "${FAILED_BUILDS[@]}"; do
|
||||
echo -e " ${RED}✗${NC} $variant"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If pushing was enabled, show the published tags
|
||||
if [ "$PUSH_IMAGES" = "true" ]; then
|
||||
echo -e "\n${GREEN}Images successfully published to $DOCKER_REPO${NC}"
|
||||
echo "You can pull them with:"
|
||||
for variant in "${!VARIANTS[@]}"; do
|
||||
suffix="${VARIANTS[$variant]}"
|
||||
tags=$(determine_tags "$variant" "$suffix")
|
||||
for tag in $tags; do
|
||||
echo " docker pull $DOCKER_REPO:$tag"
|
||||
done
|
||||
done
|
||||
else
|
||||
echo -e "\n${YELLOW}Images built locally (not pushed)${NC}"
|
||||
echo "To push images, set PUSH_IMAGES=true"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Done!${NC}"
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "Building and testing all Bun Docker images..."
|
||||
|
||||
# Get the latest Bun version or use a specified one
|
||||
BUN_VERSION=${1:-latest}
|
||||
echo "Using Bun version: $BUN_VERSION"
|
||||
|
||||
# Array of variants to test
|
||||
VARIANTS=("debian" "debian-slim" "alpine" "distroless")
|
||||
|
||||
# Track results
|
||||
FAILED_VARIANTS=()
|
||||
PASSED_VARIANTS=()
|
||||
|
||||
# Function to test a Docker image
|
||||
test_docker_image() {
|
||||
local variant=$1
|
||||
local dir=$2
|
||||
local tag="bun-test:$variant"
|
||||
|
||||
echo -e "\n${YELLOW}Testing $variant...${NC}"
|
||||
|
||||
# Build the image
|
||||
echo "Building $variant image..."
|
||||
if ! docker build -t "$tag" "$dir" --build-arg BUN_VERSION="$BUN_VERSION" 2>&1 | tail -20; then
|
||||
echo -e "${RED}✗ Failed to build $variant${NC}"
|
||||
FAILED_VARIANTS+=("$variant (build failed)")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test 1: Check if bun is installed and works
|
||||
echo "Testing bun command..."
|
||||
if ! docker run --rm "$tag" bun --version; then
|
||||
echo -e "${RED}✗ bun --version failed for $variant${NC}"
|
||||
FAILED_VARIANTS+=("$variant (bun command failed)")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test 2: Check if bunx works
|
||||
echo "Testing bunx command..."
|
||||
if ! docker run --rm "$tag" sh -c 'which bunx && bunx --version' 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠ bunx not available in $variant (may be expected for distroless)${NC}"
|
||||
fi
|
||||
|
||||
# Test 3: Test a simple JavaScript execution
|
||||
echo "Testing JavaScript execution..."
|
||||
if ! docker run --rm "$tag" bun eval 'console.log("Hello from Bun!")'; then
|
||||
echo -e "${RED}✗ JavaScript execution failed for $variant${NC}"
|
||||
FAILED_VARIANTS+=("$variant (JS execution failed)")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test 4: Check if git is installed (except distroless)
|
||||
if [ "$variant" != "distroless" ]; then
|
||||
echo "Testing git availability..."
|
||||
if ! docker run --rm "$tag" sh -c 'which git' 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠ git not available in $variant${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 5: Test package installation
|
||||
echo "Testing package installation..."
|
||||
if ! docker run --rm "$tag" sh -c 'echo "{\"name\":\"test\",\"dependencies\":{\"is-number\":\"*\"}}" > package.json && bun install --no-save 2>&1 | grep -q "is-number"' 2>/dev/null; then
|
||||
if [ "$variant" = "distroless" ]; then
|
||||
echo -e "${YELLOW}⚠ Package installation test skipped for distroless (no shell)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Package installation may have issues in $variant${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 6: Check multi-arch support
|
||||
echo "Checking image architecture..."
|
||||
docker run --rm "$tag" sh -c 'uname -m' 2>/dev/null || echo "Architecture check skipped (no shell)"
|
||||
|
||||
# Test 7: Security scan with trivy (if available)
|
||||
if command -v trivy &> /dev/null; then
|
||||
echo "Running security scan..."
|
||||
trivy image --severity HIGH,CRITICAL --no-progress "$tag" 2>/dev/null | grep -E "Total:|HIGH:|CRITICAL:" || echo "No HIGH/CRITICAL vulnerabilities found"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ $variant tests passed${NC}"
|
||||
PASSED_VARIANTS+=("$variant")
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test each variant
|
||||
for variant in "${VARIANTS[@]}"; do
|
||||
# Determine the directory
|
||||
if [ "$variant" = "debian-slim" ]; then
|
||||
dir="./debian-slim"
|
||||
elif [ "$variant" = "debian" ]; then
|
||||
dir="./debian"
|
||||
else
|
||||
dir="./$variant"
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo -e "${RED}✗ Directory $dir not found for $variant${NC}"
|
||||
FAILED_VARIANTS+=("$variant (directory not found)")
|
||||
continue
|
||||
fi
|
||||
|
||||
test_docker_image "$variant" "$dir" || true
|
||||
done
|
||||
|
||||
# Print summary
|
||||
echo -e "\n${YELLOW}========== TEST SUMMARY ==========${NC}"
|
||||
echo -e "${GREEN}Passed: ${#PASSED_VARIANTS[@]} variants${NC}"
|
||||
for variant in "${PASSED_VARIANTS[@]}"; do
|
||||
echo -e " ${GREEN}✓${NC} $variant"
|
||||
done
|
||||
|
||||
if [ ${#FAILED_VARIANTS[@]} -gt 0 ]; then
|
||||
echo -e "${RED}Failed: ${#FAILED_VARIANTS[@]} variants${NC}"
|
||||
for variant in "${FAILED_VARIANTS[@]}"; do
|
||||
echo -e " ${RED}✗${NC} $variant"
|
||||
done
|
||||
exit 1
|
||||
else
|
||||
echo -e "\n${GREEN}All Docker images built and tested successfully!${NC}"
|
||||
fi
|
||||
|
||||
# Cleanup test images
|
||||
echo -e "\nCleaning up test images..."
|
||||
for variant in "${VARIANTS[@]}"; do
|
||||
docker rmi "bun-test:$variant" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "root.h"
|
||||
#include "JSBundlerPlugin.h"
|
||||
|
||||
#include "BunProcess.h"
|
||||
@@ -57,6 +58,7 @@ JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onLoadAsync);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onResolveAsync);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_onBeforeParse);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_generateDeferPromise);
|
||||
JSC_DECLARE_HOST_FUNCTION(jsBundlerPluginFunction_addVirtualModule);
|
||||
|
||||
void BundlerPlugin::NamespaceList::append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, unsigned& index)
|
||||
{
|
||||
@@ -101,6 +103,17 @@ static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& li
|
||||
}
|
||||
bool BundlerPlugin::anyMatchesCrossThread(JSC::VM& vm, const BunString* namespaceStr, const BunString* path, bool isOnLoad)
|
||||
{
|
||||
auto pathString = path->toWTFString(BunString::ZeroCopy);
|
||||
|
||||
// Check virtual modules for both onLoad and onResolve (only in "file" namespace)
|
||||
if (!this->virtualModules.isEmpty()) {
|
||||
// Virtual modules only work in the "file" namespace or empty namespace (defaults to "file")
|
||||
bool isFileNamespace = !namespaceStr || namespaceStr->isEmpty() || namespaceStr->toWTFString(BunString::ZeroCopy) == "file"_s;
|
||||
if (isFileNamespace && this->virtualModules.contains(pathString)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOnLoad) {
|
||||
return anyMatchesForNamespace(vm, this->onLoad, namespaceStr, path);
|
||||
} else {
|
||||
@@ -108,6 +121,43 @@ bool BundlerPlugin::anyMatchesCrossThread(JSC::VM& vm, const BunString* namespac
|
||||
}
|
||||
}
|
||||
|
||||
bool BundlerPlugin::hasVirtualModule(const String& path) const
|
||||
{
|
||||
return !virtualModules.isEmpty() && virtualModules.contains(path);
|
||||
}
|
||||
|
||||
JSC::JSObject* BundlerPlugin::getVirtualModule(const String& path)
|
||||
{
|
||||
if (virtualModules.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto it = virtualModules.find(path);
|
||||
if (it != virtualModules.end()) {
|
||||
unsigned index = it->value;
|
||||
if (index < virtualModulesList.list().size()) {
|
||||
return virtualModulesList.list()[index].get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BundlerPlugin::addVirtualModule(JSC::VM& vm, JSC::JSCell* owner, const String& path, JSC::JSObject* moduleFunction)
|
||||
{
|
||||
unsigned index = virtualModulesList.list().size();
|
||||
virtualModulesList.append(vm, owner, moduleFunction);
|
||||
virtualModules.set(path, index);
|
||||
}
|
||||
|
||||
void BundlerPlugin::tombstone()
|
||||
{
|
||||
tombstoned = true;
|
||||
virtualModules.clear();
|
||||
// virtualModulesList will be cleaned up by destructor
|
||||
}
|
||||
|
||||
// Template implementation moved to header file
|
||||
|
||||
static const HashTableValue JSBundlerPluginHashTable[] = {
|
||||
{ "addFilter"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addFilter, 3 } },
|
||||
{ "addError"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addError, 3 } },
|
||||
@@ -115,6 +165,7 @@ static const HashTableValue JSBundlerPluginHashTable[] = {
|
||||
{ "onResolveAsync"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_onResolveAsync, 4 } },
|
||||
{ "onBeforeParse"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_onBeforeParse, 4 } },
|
||||
{ "generateDeferPromise"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_generateDeferPromise, 0 } },
|
||||
{ "addVirtualModule"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBundlerPluginFunction_addVirtualModule, 2 } },
|
||||
};
|
||||
|
||||
class JSBundlerPlugin final : public JSC::JSDestructibleObject {
|
||||
@@ -193,7 +244,7 @@ void JSBundlerPlugin::visitAdditionalChildren(Visitor& visitor)
|
||||
this->onLoadFunction.visit(visitor);
|
||||
this->onResolveFunction.visit(visitor);
|
||||
this->setupFunction.visit(visitor);
|
||||
this->plugin.deferredPromises.visit(this, visitor);
|
||||
this->plugin.visitAdditionalChildren(this, visitor);
|
||||
}
|
||||
|
||||
template<typename Visitor>
|
||||
@@ -467,6 +518,37 @@ JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_generateDeferPromise, (JSC::JSG
|
||||
return encoded_defer_promise;
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_addVirtualModule, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
|
||||
{
|
||||
JSBundlerPlugin* thisObject = jsCast<JSBundlerPlugin*>(callFrame->thisValue());
|
||||
if (thisObject->plugin.tombstoned) {
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSC::JSValue pathValue = callFrame->argument(0);
|
||||
JSC::JSValue moduleValue = callFrame->argument(1);
|
||||
|
||||
if (!pathValue.isString()) {
|
||||
throwTypeError(globalObject, scope, "Expected first argument to be a string"_s);
|
||||
return JSC::JSValue::encode({});
|
||||
}
|
||||
|
||||
if (!moduleValue.isObject() || !moduleValue.isCallable()) {
|
||||
throwTypeError(globalObject, scope, "Expected second argument to be a function"_s);
|
||||
return JSC::JSValue::encode({});
|
||||
}
|
||||
|
||||
WTF::String path = pathValue.toWTFString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
thisObject->plugin.addVirtualModule(vm, thisObject, path, JSC::asObject(moduleValue));
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
void JSBundlerPlugin::finishCreation(JSC::VM& vm)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
@@ -738,4 +820,29 @@ extern "C" JSC::JSGlobalObject* JSBundlerPlugin__globalObject(Bun::JSBundlerPlug
|
||||
return plugin->m_globalObject;
|
||||
}
|
||||
|
||||
extern "C" bool JSBundlerPlugin__hasVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
return plugin->plugin.hasVirtualModule(pathStr);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue JSBundlerPlugin__getVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
auto* virtualModule = plugin->plugin.getVirtualModule(pathStr);
|
||||
if (virtualModule) {
|
||||
return JSC::JSValue::encode(virtualModule);
|
||||
}
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
extern "C" void JSBundlerPlugin__addVirtualModule(Bun::JSBundlerPlugin* plugin, const BunString* path, JSC::EncodedJSValue encodedModuleFunction)
|
||||
{
|
||||
WTF::String pathStr = path ? path->toWTFString(BunString::ZeroCopy) : WTF::String();
|
||||
JSC::JSValue moduleFunction = JSC::JSValue::decode(encodedModuleFunction);
|
||||
if (moduleFunction.isObject()) {
|
||||
plugin->plugin.addVirtualModule(plugin->vm(), plugin, pathStr, JSC::asObject(moduleFunction));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
@@ -20,6 +20,8 @@ using namespace JSC;
|
||||
|
||||
class BundlerPlugin final {
|
||||
public:
|
||||
using VirtualModuleMap = WTF::UncheckedKeyHashMap<String, unsigned>;
|
||||
|
||||
/// In native plugins, the regular expression could be called concurrently on multiple threads.
|
||||
/// Therefore, we need a mutex to synchronize access.
|
||||
class FilterRegExp {
|
||||
@@ -119,7 +121,16 @@ public:
|
||||
|
||||
public:
|
||||
bool anyMatchesCrossThread(JSC::VM&, const BunString* namespaceStr, const BunString* path, bool isOnLoad);
|
||||
void tombstone() { tombstoned = true; }
|
||||
bool hasVirtualModule(const String& path) const;
|
||||
JSC::JSObject* getVirtualModule(const String& path);
|
||||
void addVirtualModule(JSC::VM& vm, JSC::JSCell* owner, const String& path, JSC::JSObject* moduleFunction);
|
||||
void tombstone();
|
||||
template<typename Visitor>
|
||||
void visitAdditionalChildren(JSC::JSCell* cell, Visitor& visitor)
|
||||
{
|
||||
deferredPromises.visit(cell, visitor);
|
||||
virtualModulesList.visit(cell, visitor);
|
||||
}
|
||||
|
||||
BundlerPlugin(void* config, BunPluginTarget target, JSBundlerPluginAddErrorCallback addError, JSBundlerPluginOnLoadAsyncCallback onLoadAsync, JSBundlerPluginOnResolveAsyncCallback onResolveAsync)
|
||||
: addError(addError)
|
||||
@@ -130,12 +141,18 @@ public:
|
||||
this->config = config;
|
||||
}
|
||||
|
||||
~BundlerPlugin()
|
||||
{
|
||||
}
|
||||
|
||||
NamespaceList onLoad = {};
|
||||
NamespaceList onResolve = {};
|
||||
NativePluginList onBeforeParse = {};
|
||||
BunPluginTarget target { BunPluginTargetBrowser };
|
||||
|
||||
WriteBarrierList<JSC::JSPromise> deferredPromises = {};
|
||||
VirtualModuleMap virtualModules {};
|
||||
WriteBarrierList<JSC::JSObject> virtualModulesList = {};
|
||||
|
||||
JSBundlerPluginAddErrorCallback addError;
|
||||
JSBundlerPluginOnLoadAsyncCallback onLoadAsync;
|
||||
@@ -144,4 +161,4 @@ public:
|
||||
bool tombstoned { false };
|
||||
};
|
||||
|
||||
} // namespace Zig
|
||||
} // namespace Bun
|
||||
|
||||
@@ -2894,6 +2894,7 @@ pub const BundleV2 = struct {
|
||||
original_target: options.Target,
|
||||
) bool {
|
||||
if (this.plugins) |plugins| {
|
||||
// anyMatches will check for virtual modules too
|
||||
if (plugins.hasAnyMatches(&import_record.path, false)) {
|
||||
// This is where onResolve plugins are enqueued
|
||||
var resolve: *jsc.API.JSBundler.Resolve = bun.default_allocator.create(jsc.API.JSBundler.Resolve) catch unreachable;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface BundlerPlugin {
|
||||
onLoad: Map<string, [RegExp, OnLoadCallback][]>;
|
||||
onResolve: Map<string, [RegExp, OnResolveCallback][]>;
|
||||
onEndCallbacks: Array<(build: Bun.BuildOutput) => void | Promise<void>> | undefined;
|
||||
virtualModules: Map<string, () => { contents: string; loader?: string }> | undefined;
|
||||
/** Binding to `JSBundlerPlugin__onLoadAsync` */
|
||||
onLoadAsync(
|
||||
internalID,
|
||||
@@ -19,6 +20,7 @@ interface BundlerPlugin {
|
||||
/** Binding to `JSBundlerPlugin__addError` */
|
||||
addError(internalID: number, error: any, which: number): void;
|
||||
addFilter(filter, namespace, number): void;
|
||||
addVirtualModule(path: string, moduleFunction: () => any): void;
|
||||
generateDeferPromise(id: number): Promise<void>;
|
||||
promises: Array<Promise<any>> | undefined;
|
||||
|
||||
@@ -112,6 +114,12 @@ export function runOnEndCallbacks(
|
||||
buildResult: Bun.BuildOutput,
|
||||
buildRejection: AggregateError | undefined,
|
||||
): Promise<void> | void {
|
||||
// Clean up virtual modules when build ends
|
||||
if (this.virtualModules) {
|
||||
this.virtualModules.clear();
|
||||
this.virtualModules = undefined;
|
||||
}
|
||||
|
||||
const callbacks = this.onEndCallbacks;
|
||||
if (!callbacks) return;
|
||||
const promises: PromiseLike<unknown>[] = [];
|
||||
@@ -347,8 +355,33 @@ export function runSetupFunction(
|
||||
onBeforeParse,
|
||||
onStart,
|
||||
resolve: notImplementedIssueFn(2771, "build.resolve()"),
|
||||
module: () => {
|
||||
throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime");
|
||||
module: (specifier: string, callback: () => { contents: string; loader?: string }) => {
|
||||
if (!(typeof specifier === "string")) {
|
||||
throw $ERR_INVALID_ARG_TYPE("specifier", "string", specifier);
|
||||
}
|
||||
if (!$isCallable(callback)) {
|
||||
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
|
||||
}
|
||||
|
||||
// Store the virtual module
|
||||
if (!self.virtualModules) {
|
||||
self.virtualModules = new Map();
|
||||
}
|
||||
|
||||
// Check for duplicate registration
|
||||
if (self.virtualModules.has(specifier)) {
|
||||
const prev = self.virtualModules.get(specifier);
|
||||
if (prev !== callback) {
|
||||
throw new TypeError(`Virtual module "${specifier}" is already registered`);
|
||||
}
|
||||
return this; // idempotent - same callback already registered
|
||||
}
|
||||
|
||||
// Register with native first; update JS map only on success
|
||||
self.addVirtualModule(specifier, callback);
|
||||
self.virtualModules.set(specifier, callback);
|
||||
|
||||
return this;
|
||||
},
|
||||
addPreload: () => {
|
||||
throw new TypeError("addPreload() is not supported in Bun.build() yet.");
|
||||
@@ -395,8 +428,21 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa
|
||||
const kind = $ImportKindIdToLabel[kindId];
|
||||
|
||||
var promiseResult: any = (async (inputPath, inputNamespace, importer, kind) => {
|
||||
var { onResolve, onLoad } = this;
|
||||
var { onResolve, onLoad, virtualModules } = this;
|
||||
|
||||
// Check for virtual modules first (in file namespace or empty namespace)
|
||||
if (virtualModules && (!inputNamespace || inputNamespace === "file") && virtualModules.has(inputPath)) {
|
||||
this.onResolveAsync(internalID, inputPath, "file", false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!onResolve) {
|
||||
this.onResolveAsync(internalID, null, null, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
var results = onResolve.$get(inputNamespace);
|
||||
|
||||
if (!results) {
|
||||
this.onResolveAsync(internalID, null, null, null);
|
||||
return null;
|
||||
@@ -511,6 +557,74 @@ export function runOnLoadPlugins(
|
||||
|
||||
const generateDefer = () => this.generateDeferPromise(internalID);
|
||||
var promiseResult = (async (internalID, path, namespace, isServerSide, defaultLoader, generateDefer) => {
|
||||
// Check for virtual modules first (file namespace and in virtualModules map)
|
||||
if (this.virtualModules && this.virtualModules.has(path) && namespace === "file") {
|
||||
const virtualModuleCallback = this.virtualModules.get(path);
|
||||
if (virtualModuleCallback) {
|
||||
let result;
|
||||
try {
|
||||
result = virtualModuleCallback();
|
||||
} catch (e) {
|
||||
// If the callback throws, report it as an error
|
||||
this.addError(internalID, e, 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Unwrap/await promises like onLoad
|
||||
while (
|
||||
result &&
|
||||
$isPromise(result) &&
|
||||
($getPromiseInternalField(result, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled
|
||||
) {
|
||||
result = $getPromiseInternalField(result, $promiseFieldReactionsOrResult);
|
||||
}
|
||||
if (result && $isPromise(result)) {
|
||||
result = await result;
|
||||
}
|
||||
if (!result || !$isObject(result)) {
|
||||
throw new TypeError(`Virtual module "${path}" must return an object with "contents" property`);
|
||||
}
|
||||
|
||||
var { contents, loader = "js" } = result as any;
|
||||
if ((loader as any) === "object") {
|
||||
if (!("exports" in result)) {
|
||||
throw new TypeError('Virtual module returning loader: "object" must have "exports" property');
|
||||
}
|
||||
try {
|
||||
contents = JSON.stringify(result.exports);
|
||||
loader = "json";
|
||||
} catch (e) {
|
||||
throw new TypeError(
|
||||
'Virtual module must return a JSON-serializable object when using loader: "object": ' + e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(typeof contents === "string") && !$isTypedArrayView(contents)) {
|
||||
throw new TypeError(
|
||||
`Virtual module "${path}" must return an object with "contents" as a string or Uint8Array`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(typeof loader === "string")) {
|
||||
throw new TypeError(`Virtual module "${path}" "loader" must be a string if provided`);
|
||||
}
|
||||
|
||||
const chosenLoader = LOADERS_MAP[loader];
|
||||
if (chosenLoader === undefined) {
|
||||
throw new TypeError(`Virtual module "${path}": Loader ${loader} is not supported.`);
|
||||
}
|
||||
|
||||
this.onLoadAsync(internalID, contents as any, chosenLoader);
|
||||
return null;
|
||||
} catch (e) {
|
||||
this.addError(internalID, e, 1);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = this.onLoad.$get(namespace);
|
||||
if (!results) {
|
||||
this.onLoadAsync(internalID, null, null);
|
||||
|
||||
@@ -402,27 +402,41 @@ describe("Bun.build", () => {
|
||||
expect(x.logs[0].position).toBeTruthy();
|
||||
});
|
||||
|
||||
test("module() throws error", async () => {
|
||||
expect(() =>
|
||||
Bun.build({
|
||||
entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
|
||||
plugins: [
|
||||
{
|
||||
name: "test",
|
||||
setup: b => {
|
||||
b.module("ad", () => {
|
||||
return {
|
||||
exports: {
|
||||
hello: "world",
|
||||
},
|
||||
loader: "object",
|
||||
};
|
||||
});
|
||||
},
|
||||
test("module() is now supported", async () => {
|
||||
const dir = tempDirWithFiles("module-api-test", {
|
||||
"entry.js": `
|
||||
import msg from "test-virtual-module";
|
||||
console.log(msg);
|
||||
`,
|
||||
});
|
||||
|
||||
// module() is now implemented and should not throw
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(String(dir), "entry.js")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "test",
|
||||
setup: b => {
|
||||
// Verify module() exists and can be called
|
||||
expect(typeof b.module).toBe("function");
|
||||
b.module("test-virtual-module", () => {
|
||||
return {
|
||||
contents: "export default 'Hello from virtual module';",
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
// Check that the virtual module content is in the output
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("Hello from virtual module");
|
||||
});
|
||||
|
||||
test("non-object plugins throw invalid argument errors", () => {
|
||||
|
||||
527
test/bundler/bundler-plugin-virtual-modules.test.ts
Normal file
527
test/bundler/bundler-plugin-virtual-modules.test.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { tempDir } from "harness";
|
||||
import * as path from "node:path";
|
||||
|
||||
test("Bun.build plugin virtual modules - basic", async () => {
|
||||
using dir = tempDir("virtual-basic", {
|
||||
"entry.ts": `
|
||||
import message from "my-virtual-module";
|
||||
console.log(message);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "virtual-module-plugin",
|
||||
setup(build) {
|
||||
build.module("my-virtual-module", () => ({
|
||||
contents: `export default "Hello from virtual module!"`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("Hello from virtual module!");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - multiple modules", async () => {
|
||||
using dir = tempDir("virtual-multiple", {
|
||||
"entry.ts": `
|
||||
import { greeting } from "virtual-greeting";
|
||||
import { name } from "virtual-name";
|
||||
console.log(greeting + " " + name);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "multi-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-greeting", () => ({
|
||||
contents: `export const greeting = "Hello";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
build.module("virtual-name", () => ({
|
||||
contents: `export const name = "World";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("Hello");
|
||||
expect(output).toContain("World");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - TypeScript", async () => {
|
||||
using dir = tempDir("virtual-typescript", {
|
||||
"entry.ts": `
|
||||
import { calculate } from "virtual-math";
|
||||
console.log(calculate(5, 10));
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "typescript-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-math", () => ({
|
||||
contents: `
|
||||
export function calculate(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
`,
|
||||
loader: "ts",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("calculate(5, 10)"); // Function call is in output
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - JSON", async () => {
|
||||
using dir = tempDir("virtual-json", {
|
||||
"entry.ts": `
|
||||
import config from "virtual-config";
|
||||
console.log(config.version);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "json-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-config", () => ({
|
||||
contents: JSON.stringify({ version: "1.2.3", enabled: true }),
|
||||
loader: "json",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("1.2.3");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - with onLoad and onResolve", async () => {
|
||||
using dir = tempDir("virtual-mixed", {
|
||||
"entry.ts": `
|
||||
import virtual from "my-virtual";
|
||||
import modified from "./real.js";
|
||||
console.log(virtual, modified);
|
||||
`,
|
||||
"real.js": `export default "original";`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "mixed-plugin",
|
||||
setup(build) {
|
||||
// Virtual module
|
||||
build.module("my-virtual", () => ({
|
||||
contents: `export default "virtual content";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
// Regular onLoad plugin
|
||||
build.onLoad({ filter: /\/real\.js$/ }, () => ({
|
||||
contents: `export default "modified";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("virtual content");
|
||||
expect(output).toContain("modified");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - dynamic content", async () => {
|
||||
using dir = tempDir("virtual-dynamic", {
|
||||
"entry.ts": `
|
||||
import timestamp from "virtual-timestamp";
|
||||
console.log(timestamp);
|
||||
`,
|
||||
});
|
||||
|
||||
const buildTime = Date.now();
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "dynamic-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-timestamp", () => ({
|
||||
contents: `export default ${buildTime};`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain(buildTime.toString());
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - nested imports", async () => {
|
||||
using dir = tempDir("virtual-nested", {
|
||||
"entry.ts": `
|
||||
import { main } from "virtual-main";
|
||||
console.log(main());
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "nested-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-main", () => ({
|
||||
contents: `
|
||||
import { helper } from "virtual-helper";
|
||||
export function main() {
|
||||
return helper() + " from main";
|
||||
}
|
||||
`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
build.module("virtual-helper", () => ({
|
||||
contents: `
|
||||
export function helper() {
|
||||
return "Hello";
|
||||
}
|
||||
`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain('helper() + " from main"'); // Check for the function composition
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - multiple plugins", async () => {
|
||||
using dir = tempDir("virtual-multi-plugin", {
|
||||
"entry.ts": `
|
||||
import first from "virtual-first";
|
||||
import second from "virtual-second";
|
||||
console.log(first, second);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "first-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-first", () => ({
|
||||
contents: `export default "from first plugin";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "second-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-second", () => ({
|
||||
contents: `export default "from second plugin";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("from first plugin");
|
||||
expect(output).toContain("from second plugin");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - error handling", async () => {
|
||||
using dir = tempDir("virtual-error", {
|
||||
"entry.ts": `
|
||||
import data from "virtual-error";
|
||||
console.log(data);
|
||||
`,
|
||||
});
|
||||
|
||||
// Plugin errors are thrown as "Bundle failed"
|
||||
await expect(
|
||||
Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "error-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-error", () => {
|
||||
throw new Error("Failed to generate virtual module");
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/Bundle failed/i);
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - CSS", async () => {
|
||||
using dir = tempDir("virtual-css", {
|
||||
"entry.ts": `
|
||||
import styles from "virtual-styles";
|
||||
console.log(styles);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "css-virtual-plugin",
|
||||
setup(build) {
|
||||
build.module("virtual-styles", () => ({
|
||||
contents: `
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
loader: "css",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(2); // JS and CSS output
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - onLoad plugins still work", async () => {
|
||||
using dir = tempDir("virtual-with-onload", {
|
||||
"entry.ts": `
|
||||
import virtual from "my-virtual";
|
||||
import data from "./real.json";
|
||||
console.log(virtual, data);
|
||||
`,
|
||||
"real.json": `{"original": "data"}`,
|
||||
});
|
||||
|
||||
let onLoadCalled = false;
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "combined-plugin",
|
||||
setup(build) {
|
||||
// Add virtual module
|
||||
build.module("my-virtual", () => ({
|
||||
contents: `export default "virtual content";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
// Also add regular onLoad plugin for JSON files
|
||||
build.onLoad({ filter: /\.json$/ }, args => {
|
||||
onLoadCalled = true;
|
||||
return {
|
||||
contents: `{"modified": "by onLoad plugin"}`,
|
||||
loader: "json",
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(onLoadCalled).toBe(true);
|
||||
|
||||
const output = await result.outputs[0].text();
|
||||
expect(output).toContain("virtual content");
|
||||
expect(output).toContain("modified");
|
||||
expect(output).toContain("by onLoad plugin");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - duplicate registration throws", async () => {
|
||||
using dir = tempDir("virtual-duplicate", {
|
||||
"entry.js": `console.log("test");`,
|
||||
});
|
||||
|
||||
await expect(
|
||||
Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.js")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "duplicate-test",
|
||||
setup(build) {
|
||||
// First registration
|
||||
build.module("duplicate-module", () => ({
|
||||
contents: `export default "first";`,
|
||||
loader: "js",
|
||||
}));
|
||||
|
||||
// Duplicate registration with different callback should throw
|
||||
expect(() => {
|
||||
build.module("duplicate-module", () => ({
|
||||
contents: `export default "second";`,
|
||||
loader: "js",
|
||||
}));
|
||||
}).toThrow('Virtual module "duplicate-module" is already registered');
|
||||
|
||||
// Same callback should be idempotent (not throw)
|
||||
const sameCallback = () => ({
|
||||
contents: `export default "first";`,
|
||||
loader: "js",
|
||||
});
|
||||
build.module("another-module", sameCallback);
|
||||
build.module("another-module", sameCallback); // Should not throw
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).resolves.toHaveProperty("success", true);
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - virtual module as entrypoint", async () => {
|
||||
using dir = tempDir("virtual-entrypoint", {});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["virtual-entry"],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: "in-memory-entrypoint",
|
||||
setup(build) {
|
||||
build.module("virtual-entry", () => ({
|
||||
contents: `console.log("Hello from virtual entrypoint");`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
const output = await Bun.file(result.outputs[0].path).text();
|
||||
expect(output).toContain("Hello from virtual entrypoint");
|
||||
});
|
||||
|
||||
test("Bun.build plugin virtual modules - no memory leak on repeated builds", async () => {
|
||||
using dir = tempDir("virtual-memory", {
|
||||
"entry.ts": `
|
||||
import msg from "test-module";
|
||||
console.log(msg);
|
||||
`,
|
||||
});
|
||||
|
||||
// Track memory usage with multiple builds
|
||||
const initialMemory = process.memoryUsage().heapUsed;
|
||||
const memoryAfterBuilds = [];
|
||||
|
||||
// Run multiple builds to check for memory leaks
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.ts")],
|
||||
outdir: String(dir),
|
||||
plugins: [
|
||||
{
|
||||
name: `test-plugin-${i}`,
|
||||
setup(build) {
|
||||
// Create a large callback to make memory leaks more visible
|
||||
const largeData = new Array(10000).fill(`data-${i}`);
|
||||
build.module("test-module", () => ({
|
||||
contents: `export default "${largeData[0]}";`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Force GC after each build if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
memoryAfterBuilds.push(process.memoryUsage().heapUsed);
|
||||
}
|
||||
|
||||
// Memory usage should stabilize and not continuously grow
|
||||
// Check that the last few builds don't show significant growth
|
||||
const lastThreeBuilds = memoryAfterBuilds.slice(-3);
|
||||
const avgLastThree = lastThreeBuilds.reduce((a, b) => a + b, 0) / 3;
|
||||
const firstThreeBuilds = memoryAfterBuilds.slice(0, 3);
|
||||
const avgFirstThree = firstThreeBuilds.reduce((a, b) => a + b, 0) / 3;
|
||||
|
||||
// Memory shouldn't grow by more than 50% between first and last builds
|
||||
// This is a loose check to avoid flakiness
|
||||
const memoryGrowthRatio = avgLastThree / avgFirstThree;
|
||||
expect(memoryGrowthRatio).toBeLessThan(1.5);
|
||||
});
|
||||
Reference in New Issue
Block a user