Initial push of Plunk Next

This commit is contained in:
Dries Augustyns
2025-12-01 10:00:25 +01:00
parent ff1876d580
commit 5b430fba0d
27 changed files with 1625 additions and 1682 deletions
+66 -5
View File
@@ -1,6 +1,67 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Build outputs
dist
.next
.turbo
out
# Development
.env
.next/
.github/
dist/
assets/
node_modules/
.env*.local
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Testing
coverage
.nyc_output
**/__tests__
**/*.test.ts
**/*.test.tsx
**/*.test.js
**/*.test.jsx
**/*.spec.ts
**/*.spec.tsx
**/*.spec.js
**/*.spec.jsx
test/
vitest.config.ts
# IDE
.vscode
.idea
*.swp
*.swo
*~
.DS_Store
# Git
.git
.gitignore
.gitattributes
# CI/CD
.github
.gitlab-ci.yml
# Documentation (not needed in image)
*.md
!README.md
# Docker
Dockerfile*
docker
!docker/nginx
.dockerignore
# Misc
.cursor
.eslintcache
.cache
tmp
temp
+100
View File
@@ -0,0 +1,100 @@
# ========================================
# Plunk Self-Hosting Configuration
# ========================================
# ========================================
# REQUIRED: Security & Database
# ========================================
DB_PASSWORD=changeme123
JWT_SECRET=
# ========================================
# REQUIRED: Domains
# ========================================
# Replace example.com with your domain
# Or use *.localhost for local testing
API_DOMAIN=api.example.com
DASHBOARD_DOMAIN=app.example.com
LANDING_DOMAIN=www.example.com
WIKI_DOMAIN=docs.example.com
# Set to 'true' if using HTTPS in production (behind a reverse proxy/load balancer)
# This affects how application URIs are auto-generated from domain names
USE_HTTPS=false
# ========================================
# REQUIRED: AWS SES (Email Sending)
# ========================================
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=
AWS_SES_SECRET_ACCESS_KEY=
# Configuration sets for email tracking
# SES_CONFIGURATION_SET: Default configuration with open/click tracking enabled
SES_CONFIGURATION_SET=plunk-configuration-set
# SES_CONFIGURATION_SET_NO_TRACKING: Optional configuration without tracking
# If not set, the tracking toggle will be hidden in project settings
# When set, projects can choose to disable email tracking
SES_CONFIGURATION_SET_NO_TRACKING=plunk-no-tracking-configuration-set
# ========================================
# OPTIONAL: OAuth Login
# ========================================
GITHUB_OAUTH_CLIENT=
GITHUB_OAUTH_SECRET=
GOOGLE_OAUTH_CLIENT=
GOOGLE_OAUTH_SECRET=
# ========================================
# OPTIONAL: Stripe Billing
# ========================================
STRIPE_SK=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_ONBOARDING=
STRIPE_PRICE_EMAIL_USAGE=
STRIPE_METER_EVENT_NAME=emails
# ========================================
# OPTIONAL: File Storage (Minio)
# ========================================
# Minio is included by default in Docker Compose
# These credentials match the Minio service configuration
# Leave defaults unless you're using external storage
MINIO_ROOT_USER=plunk
MINIO_ROOT_PASSWORD=plunkminiopass
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
# S3-compatible storage configuration
# For self-hosted: uses internal Minio service (defaults work out of the box)
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY_ID=plunk
S3_ACCESS_KEY_SECRET=plunkminiopass
S3_BUCKET=uploads
S3_PUBLIC_URL=http://localhost:9000/uploads
S3_FORCE_PATH_STYLE=true
# ========================================
# OPTIONAL: SMTP Server
# ========================================
# The SMTP relay server allows sending emails via SMTP protocol
# TLS certificates can be mounted via:
# 1. Traefik acme.json (requires SMTP_DOMAIN to select the right cert)
# 2. PEM files (privkey.pem and fullchain.pem)
# SMTP domain - Required if using Traefik acme.json with multiple certificates
# Optional if using PEM files
SMTP_DOMAIN=smtp.example.com
# SMTP Ports (defaults work for most setups)
# PORT_SECURE=465 # SMTPS (implicit TLS)
# PORT_SUBMISSION=587 # SMTP Submission (STARTTLS)
# Maximum recipients per email (default: 5)
# MAX_RECIPIENTS=5
# ========================================
# ADVANCED (rarely needed)
# ========================================
# NGINX_PORT=80
+45
View File
@@ -0,0 +1,45 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
ecmaVersion: 2024,
sourceType: 'module',
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'next/core-web-vitals',
'prettier',
],
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import'],
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{argsIgnorePattern: '^_', varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'},
],
'@typescript-eslint/consistent-type-imports': ['warn', {prefer: 'type-imports'}],
'import/order': [
'warn',
{
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling'],
'newlines-between': 'always',
'alphabetize': {order: 'asc'},
},
],
},
settings: {
react: {
version: 'detect',
},
},
ignorePatterns: ['node_modules/', 'dist/', '.next/', '*.config.js', '*.config.ts'],
};
+45
View File
@@ -0,0 +1,45 @@
## Description
<!-- Describe your changes in detail -->
## Type of Change
<!-- Mark the appropriate option with an 'x' -->
- [ ] `feat:` New feature (MINOR version bump)
- [ ] `fix:` Bug fix (PATCH version bump)
- [ ] `feat!:` Breaking change - new feature (MAJOR version bump)
- [ ] `fix!:` Breaking change - bug fix (MAJOR version bump)
- [ ] `docs:` Documentation update (no version bump)
- [ ] `chore:` Maintenance/dependencies (no version bump)
- [ ] `refactor:` Code refactoring (no version bump)
- [ ] `test:` Adding tests (no version bump)
- [ ] `perf:` Performance improvement (PATCH version bump)
## PR Title Format
<!--
Your PR title should follow conventional commits format:
✅ feat: add email template editor
✅ fix: resolve memory leak in worker
✅ feat!: redesign API authentication
❌ Added new feature (missing type prefix)
See VERSIONING.md for more details
-->
## Testing
<!-- Describe how you tested your changes -->
## Checklist
- [ ] PR title follows conventional commits format
- [ ] Code builds successfully
- [ ] Tests pass locally
- [ ] Documentation updated (if needed)
## Related Issues
<!-- Link any related issues here -->
Closes #
+197
View File
@@ -0,0 +1,197 @@
name: CI
on:
push:
branches:
- next
pull_request:
branches:
- next
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
# Service containers for database, Redis, and Minio
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: plunk_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
minio:
image: minio/minio:edge-cicd
env:
MINIO_ROOT_USER: plunk
MINIO_ROOT_PASSWORD: plunkminiopass
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://localhost:9000/minio/health/live || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Setup environment variables
run: |
cat > .env << EOF
NODE_ENV=test
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk_test
DIRECT_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk_test
# Redis
REDIS_URL=redis://localhost:6379
# Security
JWT_SECRET=test-jwt-secret-for-ci-only
# S3 (Minio)
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY_ID=plunk
S3_ACCESS_KEY_SECRET=plunkminiopass
S3_BUCKET=uploads
S3_PUBLIC_URL=http://localhost:9000/uploads
S3_FORCE_PATH_STYLE=true
# Application URLs (not needed for tests but required by schema)
API_URI=http://localhost:8080
DASHBOARD_URI=http://localhost:3000
LANDING_URI=http://localhost:4000
WIKI_URI=http://localhost:1000
# AWS SES (mock values for tests)
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=mock
AWS_SES_SECRET_ACCESS_KEY=mock
SES_CONFIGURATION_SET=test
SES_CONFIGURATION_SET_NO_TRACKING=test-no-tracking
EOF
- name: Build shared packages
run: yarn build --filter="@plunk/shared" --filter="@plunk/db"
- name: Generate Prisma Client
run: yarn workspace @plunk/db db:generate
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
- name: Run database migrations
run: yarn workspace @plunk/db migrate:prod
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
- name: Create MinIO bucket
run: |
docker run --rm --network host \
--entrypoint /bin/sh minio/mc:latest \
-c "mc alias set local http://localhost:9000 plunk plunkminiopass && \
mc mb local/uploads --ignore-existing"
- name: Run tests
run: yarn test:run
env:
NODE_ENV: test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
coverage/
**/*.test.ts.log
retention-days: 7
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Setup minimal env (for build)
run: |
cat > .env << EOF
NODE_ENV=development
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk
DIRECT_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk
REDIS_URL=redis://localhost:6379
JWT_SECRET=test
# S3 (minimal - not needed but schema may require)
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY_ID=mock
S3_ACCESS_KEY_SECRET=mock
S3_BUCKET=uploads
S3_PUBLIC_URL=http://localhost:9000/uploads
S3_FORCE_PATH_STYLE=true
# Application URLs
API_URI=http://localhost:8080
DASHBOARD_URI=http://localhost:3000
LANDING_URI=http://localhost:4000
WIKI_URI=http://localhost:1000
# AWS SES (mock)
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=mock
AWS_SES_SECRET_ACCESS_KEY=mock
SES_CONFIGURATION_SET=test
SES_CONFIGURATION_SET_NO_TRACKING=test
EOF
- name: Build shared packages
run: yarn build --filter="@plunk/shared" --filter="@plunk/db"
- name: Run linter
run: yarn lint
- name: Type check
run: yarn build --filter="api" --filter="web" --filter="landing" --filter="wiki" || true
-32
View File
@@ -1,32 +0,0 @@
name: Build and Push Docker image (Canary)
on:
push:
branches:
- canary
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: |
driaug/plunk:canary
platforms: linux/amd64,linux/arm64
-37
View File
@@ -1,37 +0,0 @@
name: Build and Push Docker image (Production)
on:
push:
branches:
- main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Get version from package.json
id: get_version
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: |
driaug/plunk:${{ env.VERSION }}
driaug/plunk:latest
platforms: linux/amd64,linux/arm64
+134
View File
@@ -0,0 +1,134 @@
name: Docker Build and Publish
on:
push:
branches:
- next
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Turborepo cache
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Check if this commit is a release
id: check-release
run: |
git fetch --tags
# Check if this is a release-please commit
if git log -1 --pretty=%B | grep -q "release-please--branches--next"; then
echo "This is a release commit, waiting for tag..."
# Wait up to 60 seconds for release-please to create the tag
for i in {1..12}; do
sleep 5
git fetch --tags
TAG=$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || echo "")
if [[ -n "$TAG" ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
echo "release_tag=$TAG" >> $GITHUB_OUTPUT
echo "Found release tag: $TAG"
exit 0
fi
echo "Waiting for tag... ($i/12)"
done
echo "ERROR: Release commit but no tag found after 60s"
exit 1
else
# Regular commit, check for tag immediately
TAG=$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || echo "")
if [[ -n "$TAG" ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
echo "release_tag=$TAG" >> $GITHUB_OUTPUT
echo "This is a release: $TAG"
else
echo "is_release=false" >> $GITHUB_OUTPUT
echo "This is a regular commit"
fi
fi
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
docker system prune -af --volumes
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute tags
id: tags
run: |
if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then
# This is a release: use semver tags
TAG="${{ steps.check-release.outputs.release_tag }}"
VERSION="${TAG#v}"
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
TAGS="${VERSION},${MAJOR}.${MINOR},${MAJOR},latest"
echo "Building RELEASE with tags: $TAGS"
else
# Regular commit: use SHA tags
SHORT_SHA="${GITHUB_SHA:0:7}"
TAGS="sha-${SHORT_SHA},latest"
echo "Building COMMIT with tags: $TAGS"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push
run: |
IMAGE="ghcr.io/${{ github.repository }}"
TAGS="${{ steps.tags.outputs.tags }}"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
$(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--build-arg BUILDTIME="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--build-arg VERSION="${{ github.ref_name }}" \
--build-arg REVISION="${{ github.sha }}" \
.
- name: Summary
run: |
IMAGE="ghcr.io/${{ github.repository }}"
TAGS="${{ steps.tags.outputs.tags }}"
echo "## Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then
echo "**Type:** Release (${{ steps.check-release.outputs.release_tag }})" >> $GITHUB_STEP_SUMMARY
else
echo "**Type:** Commit (sha-${GITHUB_SHA:0:7})" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$TAGS" | tr ',' '\n' | sed "s|^|${IMAGE}:|" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
+24
View File
@@ -0,0 +1,24 @@
name: Release Please
on:
push:
branches:
- next
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release Please
uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
+46 -8
View File
@@ -1,12 +1,50 @@
# Dependencies
node_modules
dist
.idea
.vscode
*.log
.pnp
.pnp.js
# Local env files
.env
.tscache
.next
.out
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
.source/
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
.yarn/install-state.gz
*.pem
.idea/
.vscode/
robots.txt
*sitemap*.xml
.contentlayer
.content-collections
.source
install-state.gz
tsconfig.tsbuildinfo
+10
View File
@@ -0,0 +1,10 @@
{
"printWidth": 120,
"quoteProps": "consistent",
"arrowParens": "avoid",
"singleQuote": true,
"bracketSpacing": false,
"useTabs": false,
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
+3
View File
@@ -0,0 +1,3 @@
{
".": "0.0.1"
}
-894
View File
File diff suppressed because one or more lines are too long
+948
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -1,3 +1,9 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.3.0.cjs
# Support multiple architectures - download pre-built binaries instead of compiling
supportedArchitectures:
os: [ "linux", "darwin" ]
cpu: [ "x64", "arm64" ]
libc: [ "glibc" ]
yarnPath: .yarn/releases/yarn-4.9.1.cjs
File diff suppressed because one or more lines are too long
-33
View File
@@ -1,33 +0,0 @@
// source.config.ts
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
// lib/remark-replace-env.mjs
import { visit } from "unist-util-visit";
var API_URL = process.env.NEXT_PUBLIC_API_URI || "https://api.useplunk.com";
var DASHBOARD_URL = process.env.NEXT_PUBLIC_DASHBOARD_URI || "https://app.useplunk.com";
function remarkReplaceEnv() {
return (tree) => {
visit(tree, ["code", "inlineCode", "text", "link"], (node) => {
if (node.value && typeof node.value === "string") {
node.value = node.value.replace(/\{\{API_URL\}\}/g, API_URL).replace(/\{\{DASHBOARD_URL\}\}/g, DASHBOARD_URL);
}
if (node.url && typeof node.url === "string") {
node.url = node.url.replace(/\{\{API_URL\}\}/g, API_URL).replace(/\{\{DASHBOARD_URL\}\}/g, DASHBOARD_URL);
}
});
};
}
// source.config.ts
var docs = defineDocs({
dir: "content/docs"
});
var source_config_default = defineConfig({
mdxOptions: {
remarkPlugins: [remarkReplaceEnv]
}
});
export {
source_config_default as default,
docs
};
File diff suppressed because one or more lines are too long
-224
View File
@@ -1,224 +0,0 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}
-87
View File
@@ -1,87 +0,0 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selector that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);
-1
View File
@@ -1 +0,0 @@
{}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 B

-101
View File
@@ -1,101 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-11-30T17:34:40.516Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

-210
View File
@@ -1,210 +0,0 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
// it will be treated as a plain text search
let searchRegex;
try {
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
} catch (error) {
searchRegex = null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
let isMatch = false;
if (searchRegex) {
// If a valid regex was created, use it for matching
isMatch = searchRegex.test(row.textContent);
} else {
// Otherwise, fall back to the original plain text search
isMatch = row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase());
}
row.style.display = isMatch ? '' : 'none';
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);