mirror of
https://github.com/selfxyz/self.git
synced 2026-01-06 21:34:13 -05:00
Merge pull request #1537 from selfxyz/staging
Release to Production - 2025-12-28
This commit is contained in:
@@ -136,8 +136,9 @@ app/android/android-passport-nfc-reader/app/src/main/assets/tessdata/
|
||||
# Development & Testing
|
||||
# ========================================
|
||||
|
||||
# Test coverage
|
||||
# Test coverage (but allow docs/coverage for docstring reports)
|
||||
**/coverage/
|
||||
!docs/coverage/
|
||||
**/.nyc_output/
|
||||
|
||||
# Test files (optional - you might want AI to see tests)
|
||||
@@ -261,6 +262,9 @@ circuits/ptau/
|
||||
!metro.config.*
|
||||
!tamagui.config.ts
|
||||
|
||||
# Allow docstring coverage reports (tracked in git for coverage tracking)
|
||||
!docs/coverage/*.json
|
||||
|
||||
# Ensure source code is accessible
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
|
||||
10
.github/actionlint.yaml
vendored
Normal file
10
.github/actionlint.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Actionlint configuration to register custom runner labels
|
||||
# This prevents actionlint warnings about unrecognized runner labels
|
||||
|
||||
# Custom runner labels used in this repository
|
||||
labels:
|
||||
# Namespace-managed Apple Silicon runners
|
||||
- namespace-profile-apple-silicon-6cpu
|
||||
|
||||
# High-memory runners for circuit compilation
|
||||
- "128ram"
|
||||
2
.github/actions/mobile-setup/action.yml
vendored
2
.github/actions/mobile-setup/action.yml
vendored
@@ -59,7 +59,7 @@ runs:
|
||||
|
||||
# Configure Yarn
|
||||
corepack enable
|
||||
yarn set version 4.6.0
|
||||
yarn set version 4.12.0
|
||||
|
||||
echo "📦 Installing JavaScript dependencies with strict lock file..."
|
||||
if ! yarn install --immutable --inline-builds; then
|
||||
|
||||
2
.github/actions/yarn-install/action.yml
vendored
2
.github/actions/yarn-install/action.yml
vendored
@@ -15,7 +15,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
# Ensure we're using the correct version
|
||||
yarn --version
|
||||
|
||||
|
||||
6
.github/workflows/core-sdk-ci.yml
vendored
6
.github/workflows/core-sdk-ci.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Setup Corepack
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Setup Corepack
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
- name: Setup Corepack
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
- name: Restore build artifacts
|
||||
id: build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
|
||||
35
.github/workflows/mobile-bundle-analysis.yml
vendored
35
.github/workflows/mobile-bundle-analysis.yml
vendored
@@ -8,7 +8,21 @@ env:
|
||||
NODE_ENV: "production"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- ".github/workflows/mobile-bundle-analysis.yml"
|
||||
- ".github/actions/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
@@ -18,7 +32,7 @@ on:
|
||||
|
||||
jobs:
|
||||
analyze-android:
|
||||
runs-on: macos-latest-large
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Read and sanitize Node.js version
|
||||
@@ -83,7 +97,9 @@ jobs:
|
||||
working-directory: ./app
|
||||
|
||||
analyze-ios:
|
||||
runs-on: macos-latest-large
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||
# runs-on: macos-latest-large
|
||||
runs-on: namespace-profile-apple-silicon-6cpu
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Read and sanitize Node.js version
|
||||
@@ -122,6 +138,21 @@ jobs:
|
||||
with:
|
||||
path: app/ios/Pods
|
||||
lockfile: app/ios/Podfile.lock
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
|
||||
59
.github/workflows/mobile-ci.yml
vendored
59
.github/workflows/mobile-ci.yml
vendored
@@ -58,8 +58,8 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Activate Yarn 4.6.0
|
||||
run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Activate Yarn 4.12.0
|
||||
run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Cache Yarn
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
@@ -116,8 +116,8 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Activate Yarn 4.6.0
|
||||
run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Activate Yarn 4.12.0
|
||||
run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Cache Yarn
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
@@ -203,7 +203,10 @@ jobs:
|
||||
yarn test:ci
|
||||
working-directory: ./app
|
||||
build-ios:
|
||||
runs-on: macos-latest-large
|
||||
# This is mostly covered in mobile-e2e.yml so we don't need to run it here frequently
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
# runs-on: macos-latest-large
|
||||
runs-on: namespace-profile-apple-silicon-6cpu
|
||||
needs: build-deps
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
@@ -231,8 +234,8 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Activate Yarn 4.6.0
|
||||
run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Activate Yarn 4.12.0
|
||||
run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Set up Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
@@ -251,6 +254,21 @@ jobs:
|
||||
echo "Xcode path:"
|
||||
xcode-select -p
|
||||
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
@@ -267,8 +285,7 @@ jobs:
|
||||
- name: Cache Ruby gems
|
||||
uses: ./.github/actions/cache-bundler
|
||||
with:
|
||||
# TODO(jcortejoso): Confirm the path of the bundle cache
|
||||
path: app/ios/vendor/bundle
|
||||
path: app/vendor/bundle
|
||||
lock-file: app/Gemfile.lock
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||
- name: Cache Pods
|
||||
@@ -297,6 +314,14 @@ jobs:
|
||||
key: ${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install Mobile Dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Cache Built Dependencies
|
||||
@@ -306,6 +331,8 @@ jobs:
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
|
||||
- name: Build dependencies (cache miss)
|
||||
# if: steps.built-deps.outputs.cache-hit != 'true'
|
||||
env:
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token || '' }}
|
||||
run: |
|
||||
echo "Cache miss for built dependencies. Building now..."
|
||||
yarn workspace @selfxyz/mobile-app run build:deps
|
||||
@@ -315,14 +342,6 @@ jobs:
|
||||
bundle config set --local path 'vendor/bundle'
|
||||
bundle install --jobs 4 --retry 3
|
||||
working-directory: ./app
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install iOS Dependencies
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
@@ -405,6 +424,8 @@ jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-deps
|
||||
# This is mostly covered in mobile-e2e.yml so we don't need to run it here frequently
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -427,8 +448,8 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Activate Yarn 4.6.0
|
||||
run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Activate Yarn 4.12.0
|
||||
run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Cache Yarn
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
|
||||
18
.github/workflows/mobile-deploy.yml
vendored
18
.github/workflows/mobile-deploy.yml
vendored
@@ -265,7 +265,8 @@ jobs:
|
||||
|
||||
build-ios:
|
||||
needs: [bump-version]
|
||||
runs-on: macos-latest-large
|
||||
# runs-on: macos-latest-large
|
||||
runs-on: namespace-profile-apple-silicon-6cpu
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
@@ -430,6 +431,21 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "✅ Lock files exist"
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
|
||||
106
.github/workflows/mobile-e2e.yml
vendored
106
.github/workflows/mobile-e2e.yml
vendored
@@ -17,6 +17,15 @@ env:
|
||||
MAESTRO_VERSION: 1.41.0
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- ".github/workflows/mobile-e2e.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
@@ -26,6 +35,7 @@ on:
|
||||
- "app/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- ".github/workflows/mobile-e2e.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android-build-test:
|
||||
@@ -55,7 +65,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
@@ -136,9 +146,9 @@ jobs:
|
||||
- name: Build dependencies (outside emulator)
|
||||
run: |
|
||||
echo "Building dependencies..."
|
||||
# Ensure Yarn 4.6.0 is active
|
||||
# Ensure Yarn 4.12.0 is active
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.12.0 --activate
|
||||
yarn workspace @selfxyz/mobile-app run build:deps || { echo "❌ Dependency build failed"; exit 1; }
|
||||
echo "✅ Dependencies built successfully"
|
||||
- name: Setup Android private modules
|
||||
@@ -229,7 +239,8 @@ jobs:
|
||||
|
||||
e2e-ios:
|
||||
timeout-minutes: 120
|
||||
runs-on: macos-latest-large
|
||||
# runs-on: macos-latest-large
|
||||
runs-on: namespace-profile-apple-silicon-6cpu
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -258,7 +269,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
@@ -273,6 +284,21 @@ jobs:
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
@@ -360,7 +386,19 @@ jobs:
|
||||
- name: Verify iOS Runtime
|
||||
run: |
|
||||
echo "📱 Verifying iOS Runtime availability..."
|
||||
echo "Available iOS runtimes:"
|
||||
|
||||
# Check simctl availability (simctl without args returns non-zero, so check if tool exists)
|
||||
SIMCTL_PATH=$(xcrun -f simctl 2>/dev/null || echo "")
|
||||
if [ -z "$SIMCTL_PATH" ] || [ ! -f "$SIMCTL_PATH" ]; then
|
||||
echo "❌ simctl binary not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure simulator directories exist (required for Namespace runners)
|
||||
mkdir -p "$HOME/Library/Developer/CoreSimulator/Devices"
|
||||
mkdir -p "$HOME/Library/Developer/CoreSimulator/Caches"
|
||||
|
||||
echo "📱 Available iOS runtimes:"
|
||||
xcrun simctl list runtimes | grep iOS
|
||||
- name: Build dependencies (outside main flow)
|
||||
run: |
|
||||
@@ -383,6 +421,9 @@ jobs:
|
||||
run: |
|
||||
echo "Setting up iOS Simulator..."
|
||||
|
||||
# Ensure simulator directories exist
|
||||
mkdir -p "$HOME/Library/Developer/CoreSimulator/Devices"
|
||||
|
||||
# First, check what simulators are actually available
|
||||
echo "Available simulators:"
|
||||
xcrun simctl list devices available || {
|
||||
@@ -527,12 +568,16 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "Verifying app installation..."
|
||||
if xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1; then
|
||||
echo "✅ App successfully installed"
|
||||
# get_app_container may fail with NSPOSIXErrorDomain if app isn't ready yet - handle gracefully
|
||||
APP_CONTAINER_OUTPUT=$(xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app 2>&1) || APP_CONTAINER_EXIT=$?
|
||||
if [ -z "${APP_CONTAINER_EXIT:-}" ] && [ -n "$APP_CONTAINER_OUTPUT" ]; then
|
||||
echo "✅ App successfully installed at: $APP_CONTAINER_OUTPUT"
|
||||
else
|
||||
echo "❌ App installation verification failed"
|
||||
exit 1
|
||||
echo "⚠️ App installation verification returned exit code ${APP_CONTAINER_EXIT:-unknown} (may be expected)"
|
||||
# Check if app appears in installed apps list as fallback
|
||||
xcrun simctl listapps "$SIMULATOR_ID" 2>/dev/null | grep -i "$IOS_BUNDLE_ID" || echo "App not found in installed apps list"
|
||||
fi
|
||||
unset APP_CONTAINER_OUTPUT APP_CONTAINER_EXIT
|
||||
|
||||
echo "🚀 Testing app launch capability..."
|
||||
xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID" || {
|
||||
@@ -541,15 +586,46 @@ jobs:
|
||||
|
||||
echo "⏰ Checking simulator readiness..."
|
||||
sleep 10
|
||||
# Probe container as readiness check instead of listapps
|
||||
# Final readiness check (suppress errors to avoid annotations)
|
||||
xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1 || sleep 5
|
||||
|
||||
echo "Running Maestro tests..."
|
||||
echo "🎭 Running Maestro tests..."
|
||||
echo "Starting test execution..."
|
||||
maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml || {
|
||||
echo "Maestro test failed, but continuing to upload results..."
|
||||
|
||||
# Verify Maestro test file exists
|
||||
if [ ! -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
|
||||
echo "❌ Maestro test file not found: app/tests/e2e/launch.ios.flow.yaml"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Run Maestro with error handling for cleanup issues
|
||||
# Note: Maestro may show NSPOSIXErrorDomain code=3 errors during cleanup when
|
||||
# terminating the test runner app that's already terminated. This is harmless.
|
||||
MAESTRO_OUTPUT=$(maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
|
||||
MAESTRO_EXIT_CODE=$?
|
||||
|
||||
# Check if tests actually passed (ignore cleanup errors)
|
||||
if echo "$MAESTRO_OUTPUT" | grep -q "Flow Passed"; then
|
||||
echo "✅ Maestro tests passed"
|
||||
# Suppress harmless cleanup errors (NSPOSIXErrorDomain code=3)
|
||||
if [ $MAESTRO_EXIT_CODE -ne 0 ] && echo "$MAESTRO_OUTPUT" | grep -q "NSPOSIXErrorDomain.*code=3.*terminate"; then
|
||||
echo "⚠️ Maestro cleanup warning (harmless): Test runner termination error"
|
||||
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Maestro test failed with exit code: $MAESTRO_EXIT_CODE"
|
||||
exit 1
|
||||
fi
|
||||
elif echo "$MAESTRO_OUTPUT" | grep -q "Flow Failed"; then
|
||||
echo "❌ Maestro tests failed"
|
||||
exit 1
|
||||
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
|
||||
# Check results file if exit code is non-zero
|
||||
if [ -f "app/maestro-results.xml" ] && ! grep -q "<failure" app/maestro-results.xml; then
|
||||
echo "✅ Tests passed (cleanup error caused non-zero exit)"
|
||||
else
|
||||
echo "❌ Maestro test failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
35
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
35
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -19,6 +19,15 @@ env:
|
||||
E2E_TESTING: 1
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "packages/mobile-sdk-demo/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- ".github/workflows/mobile-sdk-demo-e2e.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
@@ -28,6 +37,7 @@ on:
|
||||
- "packages/mobile-sdk-demo/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- ".github/workflows/mobile-sdk-demo-e2e.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android-e2e:
|
||||
@@ -58,7 +68,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
@@ -202,7 +212,11 @@ jobs:
|
||||
|
||||
ios-e2e:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest-large
|
||||
# runs-on: macos-latest-large
|
||||
runs-on: namespace-profile-apple-silicon-6cpu
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
name: iOS E2E Tests Demo App
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
||||
@@ -229,7 +243,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- run: corepack prepare yarn@4.12.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
@@ -244,6 +258,21 @@ jobs:
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Check Java installation
|
||||
run: |
|
||||
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
|
||||
if command -v java &> /dev/null && java -version &> /dev/null; then
|
||||
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
|
||||
else
|
||||
echo "Java not found or not working, will install..."
|
||||
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Setup Java environment
|
||||
if: env.INSTALL_JAVA == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
|
||||
@@ -22,8 +22,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1194.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-partitions (1.1198.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -34,7 +34,7 @@ GEM
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.206.0)
|
||||
aws-sdk-s3 (1.209.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -43,7 +43,7 @@ GEM
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.16.2)
|
||||
addressable (~> 2.8)
|
||||
@@ -118,7 +118,7 @@ GEM
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -219,7 +219,7 @@ GEM
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
@@ -229,7 +229,8 @@ GEM
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.27.0)
|
||||
minitest (6.0.0)
|
||||
prism (~> 1.5)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.18.0)
|
||||
multipart-post (2.4.1)
|
||||
@@ -244,6 +245,7 @@ GEM
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
prism (1.7.0)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.8.1)
|
||||
rake (13.3.1)
|
||||
|
||||
@@ -135,7 +135,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 121
|
||||
versionName "2.9.5"
|
||||
versionName "2.9.7"
|
||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
|
||||
14
app/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
14
app/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Mobile app docstring style guide
|
||||
|
||||
Docstrings for the React Native app live alongside the source in `app/src`. We follow [TSDoc](https://tsdoc.org) conventions so that typed tooling can generate consistent API documentation.
|
||||
|
||||
## Authoring guidelines
|
||||
|
||||
- Document every exported component, hook, utility, or type alias with a leading `/** ... */` block written in the imperative mood.
|
||||
- Include `@param`, `@returns`, and `@remarks` tags when they improve clarity, especially for side-effects or platform-specific behaviour.
|
||||
- Keep examples concise. Prefer inline code blocks for short snippets and use fenced blocks only when you need multiple lines.
|
||||
- Mention platform differences explicitly (for example, “iOS only”) so consumers understand the scope of the implementation.
|
||||
|
||||
## Coverage expectations
|
||||
|
||||
Docstring coverage can be checked locally by running `yarn docstrings:app` (or `yarn docstrings` for both app and SDK). The reports generate JSON snapshots in `docs/coverage/*.json` that can be committed to track progress over time. Coverage targets are not enforced—treat the reports as guardrails to identify documentation gaps.
|
||||
@@ -396,7 +396,7 @@ The workflow consists of parallel jobs for each platform:
|
||||
|
||||
#### `build-ios` Job
|
||||
|
||||
Runs on `macos-latest-large` and performs the following steps:
|
||||
Runs on `namespace-profile-apple-silicon-6cpu` and performs the following steps:
|
||||
1. Sets up the environment (Node.js, Ruby, CocoaPods)
|
||||
2. Processes iOS secrets and certificates
|
||||
3. Runs appropriate Fastlane lane based on branch
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.5</string>
|
||||
<string>2.9.7</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -2131,7 +2131,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.53.2)
|
||||
- Yoga
|
||||
- RNSVG (15.15.0):
|
||||
- RNSVG (15.14.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2151,9 +2151,9 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.15.0)
|
||||
- RNSVG/common (= 15.14.0)
|
||||
- Yoga
|
||||
- RNSVG/common (15.15.0):
|
||||
- RNSVG/common (15.14.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2635,7 +2635,7 @@ SPEC CHECKSUMS:
|
||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
|
||||
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.5;
|
||||
MARKETING_VERSION = 2.9.7;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -686,7 +686,7 @@
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||
);
|
||||
MARKETING_VERSION = 2.9.5;
|
||||
MARKETING_VERSION = 2.9.7;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
||||
@@ -34,9 +34,8 @@ const config = {
|
||||
],
|
||||
|
||||
transformer: {
|
||||
babelTransformerPath: require.resolve(
|
||||
'react-native-svg-transformer/react-native',
|
||||
),
|
||||
babelTransformerPath:
|
||||
require.resolve('react-native-svg-transformer/react-native'),
|
||||
disableImportExportTransform: true,
|
||||
inlineRequires: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@selfxyz/mobile-app",
|
||||
"version": "2.9.5",
|
||||
"version": "2.9.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -162,8 +162,8 @@
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "^15.14.0",
|
||||
"react-native-svg-web": "^1.0.9",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "^0.19.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
@@ -237,7 +237,7 @@
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ const ModalBackDrop = styled(View, {
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export interface ModalNavigationParams
|
||||
extends Omit<ModalParams, 'onButtonPress' | 'onModalDismiss'> {
|
||||
export interface ModalNavigationParams extends Omit<
|
||||
ModalParams,
|
||||
'onButtonPress' | 'onModalDismiss'
|
||||
> {
|
||||
callbackId: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -273,6 +273,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
const navigation =
|
||||
useNavigation() as NativeStackScreenProps<RootStackParamList>['navigation'];
|
||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||
useState(false);
|
||||
const paddingBottom = useSafeBottomPadding(20);
|
||||
@@ -668,6 +670,45 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Log Level"
|
||||
description="Configure logging verbosity"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
{(['debug', 'info', 'warn', 'error'] as const).map(level => (
|
||||
<Button
|
||||
key={level}
|
||||
backgroundColor={
|
||||
loggingSeverity === level ? '$green9' : slate200
|
||||
}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={() => {
|
||||
setLoggingSeverity(level);
|
||||
}}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={loggingSeverity === level ? white : slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{loggingSeverity === level && <Check color={white} size={20} />}
|
||||
</Button>
|
||||
))}
|
||||
</YStack>
|
||||
</ParameterSection>
|
||||
|
||||
<ParameterSection
|
||||
icon={<WarningIcon color={yellow500} />}
|
||||
title="Danger Zone"
|
||||
|
||||
@@ -46,7 +46,10 @@ import {
|
||||
setDefaultDocumentTypeIfNeeded,
|
||||
usePassport,
|
||||
} from '@/providers/passportDataProvider';
|
||||
import { getPointsAddress } from '@/services/points';
|
||||
import {
|
||||
getPointsAddress,
|
||||
getWhiteListedDisclosureAddresses,
|
||||
} from '@/services/points';
|
||||
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
||||
import { ProofStatus } from '@/stores/proofTypes';
|
||||
import {
|
||||
@@ -64,6 +67,7 @@ const ProveScreen: React.FC = () => {
|
||||
const { useProvingStore, useSelfAppStore } = selfClient;
|
||||
const selectedApp = useSelfAppStore(state => state.selfApp);
|
||||
const selectedAppRef = useRef<typeof selectedApp>(null);
|
||||
const processedSessionsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false);
|
||||
const [scrollViewContentHeight, setScrollViewContentHeight] = useState(0);
|
||||
@@ -167,16 +171,41 @@ const ProveScreen: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const enhanceApp = async () => {
|
||||
const address = await getPointsAddress();
|
||||
const sessionId = selectedApp.sessionId;
|
||||
|
||||
// Only update if still the same session
|
||||
if (selectedAppRef.current?.sessionId === selectedApp.sessionId) {
|
||||
console.log('enhancing app with points address', address);
|
||||
selfClient.getSelfAppState().setSelfApp({
|
||||
...selectedApp,
|
||||
selfDefinedData: address.toLowerCase(),
|
||||
});
|
||||
if (processedSessionsRef.current.has(sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enhanceApp = async () => {
|
||||
const currentSessionId = sessionId;
|
||||
|
||||
try {
|
||||
const address = await getPointsAddress();
|
||||
const whitelistedAddresses = await getWhiteListedDisclosureAddresses();
|
||||
|
||||
const isWhitelisted = whitelistedAddresses.some(
|
||||
contract =>
|
||||
contract.contract_address.toLowerCase() === address.toLowerCase(),
|
||||
);
|
||||
|
||||
const currentApp = selfClient.getSelfAppState().selfApp;
|
||||
if (currentApp?.sessionId === currentSessionId) {
|
||||
if (isWhitelisted) {
|
||||
console.log(
|
||||
'enhancing app with whitelisted points address',
|
||||
address,
|
||||
);
|
||||
selfClient.getSelfAppState().setSelfApp({
|
||||
...currentApp,
|
||||
selfDefinedData: address.toLowerCase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processedSessionsRef.current.add(currentSessionId);
|
||||
} catch (error) {
|
||||
console.error('Failed enhancing app:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ import {
|
||||
import { interceptConsole } from '@/services/logging/logger/consoleInterceptor';
|
||||
import { lokiTransport } from '@/services/logging/logger/lokiTransport';
|
||||
import { setupNativeLoggerBridge } from '@/services/logging/logger/nativeLoggerBridge';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// Read initial logging severity from settings store
|
||||
const initialSeverity = useSettingStore.getState().loggingSeverity;
|
||||
|
||||
const defaultConfig: configLoggerType<
|
||||
transportFunctionType<object> | transportFunctionType<object>[],
|
||||
defLvlType
|
||||
> = {
|
||||
enabled: __DEV__ ? false : true,
|
||||
severity: __DEV__ ? 'debug' : 'warn', //TODO configure this using remote-config
|
||||
severity: initialSeverity,
|
||||
transport: [lokiTransport as unknown as transportFunctionType<object>],
|
||||
transportOptions: {
|
||||
colors: {
|
||||
@@ -52,6 +56,37 @@ const DocumentLogger = Logger.extend('DOCUMENT');
|
||||
//Native Modules
|
||||
const NfcLogger = Logger.extend('NFC');
|
||||
|
||||
// Collect all extended loggers for severity updates
|
||||
const extendedLoggers = [
|
||||
AppLogger,
|
||||
NotificationLogger,
|
||||
AuthLogger,
|
||||
PassportLogger,
|
||||
ProofLogger,
|
||||
SettingsLogger,
|
||||
BackupLogger,
|
||||
MockDataLogger,
|
||||
DocumentLogger,
|
||||
NfcLogger,
|
||||
];
|
||||
|
||||
// Subscribe to settings store changes to update logger severity dynamically
|
||||
// Extended loggers are independent instances, so we need to update each one
|
||||
// Note: Dynamically created loggers (e.g., in nativeLoggerBridge for unknown categories)
|
||||
// will inherit the severity at creation time but won't receive runtime updates
|
||||
let previousSeverity = initialSeverity;
|
||||
useSettingStore.subscribe(state => {
|
||||
if (state.loggingSeverity !== previousSeverity) {
|
||||
Logger.setSeverity(state.loggingSeverity);
|
||||
// Update all extended loggers since they don't inherit runtime changes
|
||||
// Extended loggers have setSeverity at runtime, even if not in type definition
|
||||
extendedLoggers.forEach(extLogger => {
|
||||
(extLogger as typeof Logger).setSeverity(state.loggingSeverity);
|
||||
});
|
||||
previousSeverity = state.loggingSeverity;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize console interceptor to route console logs to Loki
|
||||
interceptConsole(AppLogger);
|
||||
|
||||
|
||||
@@ -132,9 +132,8 @@ export const getWhiteListedDisclosureAddresses = async (): Promise<
|
||||
export const hasUserAnIdentityDocumentRegistered =
|
||||
async (): Promise<boolean> => {
|
||||
try {
|
||||
const { loadDocumentCatalogDirectlyFromKeychain } = await import(
|
||||
'@/providers/passportDataProvider'
|
||||
);
|
||||
const { loadDocumentCatalogDirectlyFromKeychain } =
|
||||
await import('@/providers/passportDataProvider');
|
||||
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
|
||||
|
||||
return catalog.documents.some(doc => doc.isRegistered === true);
|
||||
|
||||
@@ -129,12 +129,10 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
|
||||
|
||||
loadDisclosureEvents: async () => {
|
||||
try {
|
||||
const { getDisclosurePointEvents } = await import(
|
||||
'@/services/points/getEvents'
|
||||
);
|
||||
const { useProofHistoryStore } = await import(
|
||||
'@/stores/proofHistoryStore'
|
||||
);
|
||||
const { getDisclosurePointEvents } =
|
||||
await import('@/services/points/getEvents');
|
||||
const { useProofHistoryStore } =
|
||||
await import('@/stores/proofHistoryStore');
|
||||
await useProofHistoryStore.getState().initDatabase();
|
||||
const disclosureEvents = await getDisclosurePointEvents();
|
||||
const existingEvents = get().events.filter(e => e.type !== 'disclosure');
|
||||
|
||||
@@ -6,6 +6,8 @@ import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
type LoggingSeverity = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
interface PersistedSettingsState {
|
||||
addSubscribedTopic: (topic: string) => void;
|
||||
biometricsAvailable: boolean;
|
||||
@@ -19,6 +21,7 @@ interface PersistedSettingsState {
|
||||
homeScreenViewCount: number;
|
||||
incrementHomeScreenViewCount: () => void;
|
||||
isDevMode: boolean;
|
||||
loggingSeverity: LoggingSeverity;
|
||||
pointsAddress: string | null;
|
||||
removeSubscribedTopic: (topic: string) => void;
|
||||
resetBackupForPoints: () => void;
|
||||
@@ -29,6 +32,7 @@ interface PersistedSettingsState {
|
||||
setFcmToken: (token: string | null) => void;
|
||||
setHasViewedRecoveryPhrase: (viewed: boolean) => void;
|
||||
setKeychainMigrationCompleted: () => void;
|
||||
setLoggingSeverity: (severity: LoggingSeverity) => void;
|
||||
setPointsAddress: (address: string | null) => void;
|
||||
setSubscribedTopics: (topics: string[]) => void;
|
||||
setTurnkeyBackupEnabled: (turnkeyBackupEnabled: boolean) => void;
|
||||
@@ -97,6 +101,10 @@ export const useSettingStore = create<SettingsState>()(
|
||||
setDevModeOn: () => set({ isDevMode: true }),
|
||||
setDevModeOff: () => set({ isDevMode: false }),
|
||||
|
||||
loggingSeverity: __DEV__ ? 'debug' : 'warn',
|
||||
setLoggingSeverity: (severity: LoggingSeverity) =>
|
||||
set({ loggingSeverity: severity }),
|
||||
|
||||
hasCompletedKeychainMigration: false,
|
||||
setKeychainMigrationCompleted: () =>
|
||||
set({ hasCompletedKeychainMigration: true }),
|
||||
|
||||
206
app/tests/src/services/logging.test.ts
Normal file
206
app/tests/src/services/logging.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
import type { LoggingSeverity } from '@/stores/settingStore';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// Track individual logger instances to verify they all get updated
|
||||
// Must be prefixed with 'mock' to be accessible in jest.mock()
|
||||
const mockLoggerInstances = new Map<string, { setSeverity: jest.Mock }>();
|
||||
const mockRootSetSeverity = jest.fn();
|
||||
|
||||
// Mock react-native-logs
|
||||
jest.mock('react-native-logs', () => ({
|
||||
logger: {
|
||||
createLogger: jest.fn(() => ({
|
||||
setSeverity: mockRootSetSeverity,
|
||||
extend: jest.fn((name: string) => {
|
||||
const mockSetSeverity = jest.fn();
|
||||
mockLoggerInstances.set(name, { setSeverity: mockSetSeverity });
|
||||
return {
|
||||
setSeverity: mockSetSeverity,
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
extend: jest.fn(),
|
||||
};
|
||||
}),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the loki transport
|
||||
jest.mock('@/services/logging/logger/lokiTransport', () => ({
|
||||
lokiTransport: jest.fn(),
|
||||
cleanupLokiTransport: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the console interceptor
|
||||
jest.mock('@/services/logging/logger/consoleInterceptor', () => ({
|
||||
interceptConsole: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the native logger bridge
|
||||
jest.mock('@/services/logging/logger/nativeLoggerBridge', () => ({
|
||||
setupNativeLoggerBridge: jest.fn(),
|
||||
cleanup: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Logging Service - Severity Updates', () => {
|
||||
// All extended logger names that should be created
|
||||
const expectedLoggers = [
|
||||
'APP',
|
||||
'NOTIFICATION',
|
||||
'AUTH',
|
||||
'PASSPORT',
|
||||
'PROOF',
|
||||
'SETTINGS',
|
||||
'BACKUP',
|
||||
'MOCK_DATA',
|
||||
'DOCUMENT',
|
||||
'NFC',
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
// Import the logging module once before all tests
|
||||
// This triggers the creation of all loggers and sets up the subscription
|
||||
await import('@/services/logging');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Note: Don't clear mockLoggerInstances as the loggers are created only once during module import
|
||||
// Reset store to default state
|
||||
useSettingStore.setState({
|
||||
loggingSeverity: 'warn',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create all expected extended loggers', () => {
|
||||
// Verify all expected loggers were created during module import
|
||||
expect(mockLoggerInstances.size).toBe(expectedLoggers.length);
|
||||
expectedLoggers.forEach(name => {
|
||||
expect(mockLoggerInstances.has(name)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update severity on root logger and all extended loggers when settings change', async () => {
|
||||
// Clear any calls from initialization
|
||||
mockRootSetSeverity.mockClear();
|
||||
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
|
||||
|
||||
// Change the logging severity in the store
|
||||
useSettingStore.getState().setLoggingSeverity('debug');
|
||||
|
||||
// Wait for the subscription to fire
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Verify root logger was updated
|
||||
expect(mockRootSetSeverity).toHaveBeenCalledTimes(1);
|
||||
expect(mockRootSetSeverity).toHaveBeenCalledWith('debug');
|
||||
|
||||
// Verify each extended logger was updated
|
||||
mockLoggerInstances.forEach(logger => {
|
||||
expect(logger.setSeverity).toHaveBeenCalledTimes(1);
|
||||
expect(logger.setSeverity).toHaveBeenCalledWith('debug');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update each specific extended logger individually', async () => {
|
||||
mockRootSetSeverity.mockClear();
|
||||
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
|
||||
|
||||
useSettingStore.getState().setLoggingSeverity('info');
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Verify specific loggers by name
|
||||
const specificLoggers = ['APP', 'NFC', 'PASSPORT', 'PROOF'];
|
||||
specificLoggers.forEach(loggerName => {
|
||||
const logger = mockLoggerInstances.get(loggerName);
|
||||
expect(logger).toBeDefined();
|
||||
expect(logger?.setSeverity).toHaveBeenCalledWith('info');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update severity for all severity levels', async () => {
|
||||
const severityLevels: LoggingSeverity[] = [
|
||||
'debug',
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
];
|
||||
|
||||
for (const level of severityLevels) {
|
||||
mockRootSetSeverity.mockClear();
|
||||
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
|
||||
|
||||
useSettingStore.getState().setLoggingSeverity(level);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Verify root logger
|
||||
expect(mockRootSetSeverity).toHaveBeenCalledWith(level);
|
||||
|
||||
// Verify all extended loggers
|
||||
mockLoggerInstances.forEach(logger => {
|
||||
expect(logger.setSeverity).toHaveBeenCalledWith(level);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should not call setSeverity if severity has not changed', async () => {
|
||||
mockRootSetSeverity.mockClear();
|
||||
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
|
||||
|
||||
// Get current severity
|
||||
const currentSeverity = useSettingStore.getState().loggingSeverity;
|
||||
|
||||
// Set to the same severity
|
||||
useSettingStore.getState().setLoggingSeverity(currentSeverity);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Should not call setSeverity on root logger
|
||||
expect(mockRootSetSeverity).not.toHaveBeenCalled();
|
||||
|
||||
// Should not call setSeverity on any extended logger
|
||||
mockLoggerInstances.forEach(logger => {
|
||||
expect(logger.setSeverity).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle rapid severity changes correctly', async () => {
|
||||
mockRootSetSeverity.mockClear();
|
||||
mockLoggerInstances.forEach(logger => logger.setSeverity.mockClear());
|
||||
|
||||
// Rapidly change severity multiple times
|
||||
useSettingStore.getState().setLoggingSeverity('debug');
|
||||
useSettingStore.getState().setLoggingSeverity('info');
|
||||
useSettingStore.getState().setLoggingSeverity('warn');
|
||||
useSettingStore.getState().setLoggingSeverity('error');
|
||||
|
||||
// Wait for all subscriptions to fire
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Should have been called 4 times (once per change)
|
||||
expect(mockRootSetSeverity).toHaveBeenCalledTimes(4);
|
||||
|
||||
// The last call should be 'error'
|
||||
expect(mockRootSetSeverity).toHaveBeenLastCalledWith('error');
|
||||
|
||||
// Each extended logger should also have been called 4 times
|
||||
mockLoggerInstances.forEach(logger => {
|
||||
expect(logger.setSeverity).toHaveBeenCalledTimes(4);
|
||||
expect(logger.setSeverity).toHaveBeenLastCalledWith('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"ios": {
|
||||
"build": 194,
|
||||
"lastDeployed": "2025-12-14T22:52:48.122Z"
|
||||
"build": 197,
|
||||
"lastDeployed": "2025-12-25T18:27:37.342Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 126,
|
||||
"lastDeployed": "2025-12-14T22:52:48.122Z"
|
||||
"build": 127,
|
||||
"lastDeployed": "2025-12-17T16:13:30.256Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@
|
||||
"prettier": "^3.5.3",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.20.3",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
@@ -708,7 +708,7 @@
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"typechain": "^8.3.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
214
docs/coverage/app.json
Normal file
214
docs/coverage/app.json
Normal file
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"generatedAt": "2025-12-25T18:56:55.583Z",
|
||||
"label": "Mobile App",
|
||||
"totals": {
|
||||
"exports": 497,
|
||||
"documented": 75,
|
||||
"undocumented": 422,
|
||||
"coverage": 15.09
|
||||
},
|
||||
"undocumentedTotal": 422,
|
||||
"undocumentedSampled": 50,
|
||||
"undocumented": [
|
||||
{
|
||||
"file": "app/src/assets/animations/loader.ts",
|
||||
"symbol": "loadMiscAnimation"
|
||||
},
|
||||
{
|
||||
"file": "app/src/assets/animations/loader.ts",
|
||||
"symbol": "loadPassportAnimation"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/BackupDocumentationLink.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/Disclosures.tsx",
|
||||
"symbol": "default (local: Disclosures)"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/ErrorBoundary.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModal.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModalScreen.tsx",
|
||||
"symbol": "FeedbackModalScreenParams"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModalScreen.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/IdCard.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.native.tsx",
|
||||
"symbol": "SvgXml"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.native.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.web.tsx",
|
||||
"symbol": "SvgXml"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.web.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/LoadingUI.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/Mnemonic.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.tsx",
|
||||
"symbol": "PassportCameraProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.tsx",
|
||||
"symbol": "PassportCamera"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.web.tsx",
|
||||
"symbol": "PassportCameraProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.web.tsx",
|
||||
"symbol": "PassportCamera"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.tsx",
|
||||
"symbol": "QRCodeScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.tsx",
|
||||
"symbol": "QRCodeScannerView"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "QRCodeScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "QRCodeScannerView"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "FragmentProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "RCTFragmentViewManagerProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "RCTFragment"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/AadhaarNavBar.tsx",
|
||||
"symbol": "AadhaarNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "LeftAction"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "RightAction"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "NavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/DefaultNavBar.tsx",
|
||||
"symbol": "DefaultNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/DocumentFlowNavBar.tsx",
|
||||
"symbol": "DocumentFlowNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/HeadlessNavForEuclid.tsx",
|
||||
"symbol": "HeadlessNavForEuclid"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/HomeNavBar.tsx",
|
||||
"symbol": "HomeNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/IdDetailsNavBar.tsx",
|
||||
"symbol": "IdDetailsNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/Points.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/PointsNavBar.tsx",
|
||||
"symbol": "PointsNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/WebViewNavBar.tsx",
|
||||
"symbol": "WebViewNavBarProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/WebViewNavBar.tsx",
|
||||
"symbol": "WebViewNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "PointHistoryListProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "PointHistoryList"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/CopyReferralButton.tsx",
|
||||
"symbol": "CopyReferralButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/CopyReferralButton.tsx",
|
||||
"symbol": "CopyReferralButton"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralHeader.tsx",
|
||||
"symbol": "ReferralHeaderProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralHeader.tsx",
|
||||
"symbol": "ReferralHeader"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralInfo.tsx",
|
||||
"symbol": "ReferralInfoProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralInfo.tsx",
|
||||
"symbol": "ReferralInfo"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ShareButton.tsx",
|
||||
"symbol": "ShareButtonProps"
|
||||
}
|
||||
]
|
||||
}
|
||||
214
docs/coverage/sdk.json
Normal file
214
docs/coverage/sdk.json
Normal file
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"generatedAt": "2025-12-25T18:56:56.987Z",
|
||||
"label": "Mobile SDK Alpha",
|
||||
"totals": {
|
||||
"exports": 234,
|
||||
"documented": 77,
|
||||
"undocumented": 157,
|
||||
"coverage": 32.91
|
||||
},
|
||||
"undocumentedTotal": 157,
|
||||
"undocumentedSampled": 50,
|
||||
"undocumented": [
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/adapters/react-native/nfc-scanner.ts",
|
||||
"symbol": "reactNativeScannerAdapter"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/adapters/web/shims.ts",
|
||||
"symbol": "webNFCScannerShim"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.native.ts",
|
||||
"symbol": "addListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.native.ts",
|
||||
"symbol": "removeListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "EventHandler"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "NativeEventBridge"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "addListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "removeListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/AbstractButton.tsx",
|
||||
"symbol": "ButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/AbstractButton.tsx",
|
||||
"symbol": "default (local: AbstractButton)"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/HeldPrimaryButtonProveScreen.tsx",
|
||||
"symbol": "HeldPrimaryButtonProveScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/pressedStyle.tsx",
|
||||
"symbol": "pressedStyle"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButton.tsx",
|
||||
"symbol": "PrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "HeldPrimaryButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "RGBA"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "ACTION_TIMER"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "COLORS"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.tsx",
|
||||
"symbol": "HeldPrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.web.tsx",
|
||||
"symbol": "HeldPrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/SecondaryButton.tsx",
|
||||
"symbol": "SecondaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/ButtonsContainer.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/flag/RoundFlag.tsx",
|
||||
"symbol": "RoundFlag"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/Button.tsx",
|
||||
"symbol": "Button"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/Text.tsx",
|
||||
"symbol": "Text"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/View.tsx",
|
||||
"symbol": "ViewProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/View.tsx",
|
||||
"symbol": "View"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/XStack.tsx",
|
||||
"symbol": "XStack"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/YStack.tsx",
|
||||
"symbol": "YStack"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "MRZScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "MRZScannerView"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "SelfMRZScannerModule"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "FragmentProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "RCTFragmentViewManagerProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "RCTFragment"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx",
|
||||
"symbol": "NFCScannerScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx",
|
||||
"symbol": "PassportCameraScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx",
|
||||
"symbol": "QRCodeScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/TextsContainer.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Additional.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/BodyText.tsx",
|
||||
"symbol": "BodyText"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Caption.tsx",
|
||||
"symbol": "Caption"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Caution.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Description.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/DescriptionTitle.tsx",
|
||||
"symbol": "DescriptionTitle"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/styles.ts",
|
||||
"symbol": "typography"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/SubHeader.tsx",
|
||||
"symbol": "SubHeader"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Title.tsx",
|
||||
"symbol": "Title"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/config/defaults.ts",
|
||||
"symbol": "defaultConfig"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/config/merge.ts",
|
||||
"symbol": "mergeConfig"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/constants/analytics.ts",
|
||||
"symbol": "AadhaarEvents"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# Self App Development Patterns
|
||||
|
||||
## Docstring coverage workflow
|
||||
|
||||
- Run `yarn docstrings` to check documentation coverage for both the mobile app and SDK. This generates `docs/coverage/app.json` and `docs/coverage/sdk.json` so you can diff coverage changes in version control.
|
||||
- Run `yarn docstrings:app` to check only the mobile app exports.
|
||||
- Run `yarn docstrings:sdk` to focus on `@selfxyz/mobile-sdk-alpha` only.
|
||||
- Add `--details` to any command when you want a full per-file JSON breakdown for ad-hoc analysis—the default snapshots include only top-level totals and a small sample of undocumented exports to keep the tracked files compact.
|
||||
|
||||
Run the docstring reports locally before committing to track coverage changes. The reports are advisory—use them to identify documentation gaps but they won't block builds.
|
||||
|
||||
## React Native Architecture
|
||||
|
||||
### Navigation System
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"build:demo": "yarn workspace mobile-sdk-demo build",
|
||||
"build:mobile-sdk": "yarn workspace @selfxyz/mobile-sdk-alpha build",
|
||||
"check:versions": "node scripts/check-package-versions.mjs",
|
||||
"docstrings": "yarn docstrings:app && yarn docstrings:sdk",
|
||||
"docstrings:app": "yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\" --label \"Mobile App\" --write-report docs/coverage/app.json",
|
||||
"docstrings:sdk": "yarn tsx scripts/docstring-report.ts \"packages/mobile-sdk-alpha/src/**/*.{ts,tsx}\" --label \"Mobile SDK Alpha\" --write-report docs/coverage/sdk.json",
|
||||
"demo:mobile": "yarn build:mobile-sdk && yarn build:demo && yarn workspace mobile-sdk-demo start",
|
||||
"format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
|
||||
"format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false",
|
||||
@@ -65,9 +68,10 @@
|
||||
"knip": "^5.63.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
14
packages/mobile-sdk-alpha/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
14
packages/mobile-sdk-alpha/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Mobile SDK docstring style guide
|
||||
|
||||
All exported APIs from `packages/mobile-sdk-alpha/src` must carry TSDoc-compliant comments so integrators can rely on generated documentation and in-editor hints.
|
||||
|
||||
## Authoring guidelines
|
||||
|
||||
- Start each docstring with a one-line summary that describes the intent of the API in the imperative mood.
|
||||
- Describe complex parameter shapes with `@param` tags and consider linking to shared types with `{@link ...}` when the name alone is ambiguous.
|
||||
- Capture platform nuances (for example, “Android only”) and error semantics in the main description or an `@remarks` block.
|
||||
- Prefer examples that demonstrate the supported developer experience (React Native, Expo, etc.) and keep them short enough to scan quickly.
|
||||
|
||||
## Coverage expectations
|
||||
|
||||
`yarn docstrings:sdk` (or `yarn docstrings` for both app and SDK) surfaces the current coverage numbers in `docs/coverage/*.json`. The reports can be committed to track progress over time. Coverage thresholds are advisory—use the reports to plan follow-up work even when you need to land code without full documentation.
|
||||
@@ -198,7 +198,7 @@
|
||||
"react-native-svg": "*",
|
||||
"react-native-webview": "^13.16.0"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ if [ -d "$MOBILE_SDK_NATIVE" ]; then
|
||||
rm -f "dist/android/mobile-sdk-alpha-release.aar"
|
||||
fi
|
||||
|
||||
# Update submodule to latest
|
||||
echo "🔄 Updating submodule to latest..."
|
||||
git submodule update --init --recursive mobile-sdk-native
|
||||
# Setup and update submodule using the setup script
|
||||
echo "🔄 Setting up and updating submodule..."
|
||||
node scripts/setup-native-source.cjs
|
||||
|
||||
# Navigate to android directory
|
||||
cd android
|
||||
|
||||
@@ -9,6 +9,7 @@ const path = require('path');
|
||||
// Constants
|
||||
const SCRIPT_DIR = __dirname;
|
||||
const SDK_DIR = path.dirname(SCRIPT_DIR);
|
||||
const REPO_ROOT = path.resolve(SDK_DIR, '../../');
|
||||
const PRIVATE_MODULE_PATH = path.join(SDK_DIR, 'mobile-sdk-native');
|
||||
|
||||
const GITHUB_ORG = 'selfxyz';
|
||||
@@ -34,10 +35,10 @@ function log(message, type = 'info') {
|
||||
console.log(`${prefix} ${message}`);
|
||||
}
|
||||
|
||||
function runCommand(command, options = {}) {
|
||||
function runCommand(command, options = {}, cwd = SDK_DIR) {
|
||||
const defaultOptions = {
|
||||
stdio: isDryRun ? 'pipe' : 'inherit',
|
||||
cwd: SDK_DIR,
|
||||
cwd: cwd,
|
||||
encoding: 'utf8',
|
||||
...options,
|
||||
};
|
||||
@@ -112,18 +113,98 @@ function setupSubmodule() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if submodule already exists
|
||||
if (fs.existsSync(PRIVATE_MODULE_PATH)) {
|
||||
log('Submodule already exists, updating...', 'info');
|
||||
runCommand(`git submodule update --init --recursive mobile-sdk-native`);
|
||||
// Check if submodule is registered in .gitmodules (at repo root)
|
||||
const gitmodulesPath = path.join(REPO_ROOT, '.gitmodules');
|
||||
const gitmodulesExists = fs.existsSync(gitmodulesPath);
|
||||
const gitmodulesContent = gitmodulesExists ? fs.readFileSync(gitmodulesPath, 'utf8') : '';
|
||||
const isSubmoduleRegistered =
|
||||
gitmodulesExists && gitmodulesContent.includes('[submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]');
|
||||
|
||||
if (process.env.DEBUG_SETUP === 'true') {
|
||||
log(`Environment: CI=${isCI}, hasAppToken=${!!appToken}, hasRepoToken=${!!repoToken}`, 'info');
|
||||
log(`Submodule registered: ${isSubmoduleRegistered}`, 'info');
|
||||
}
|
||||
|
||||
// Check if submodule directory exists and has content
|
||||
const submoduleExists = fs.existsSync(PRIVATE_MODULE_PATH);
|
||||
let submoduleHasContent = false;
|
||||
try {
|
||||
submoduleHasContent = submoduleExists && fs.readdirSync(PRIVATE_MODULE_PATH).length > 0;
|
||||
} catch {
|
||||
// Directory might not be readable, treat as empty
|
||||
submoduleHasContent = false;
|
||||
}
|
||||
|
||||
log(`Submodule directory exists: ${submoduleExists}, has content: ${submoduleHasContent}`, 'info');
|
||||
|
||||
// If submodule is registered, update its URL first (important for CI where we switch from SSH to HTTPS)
|
||||
if (isSubmoduleRegistered) {
|
||||
log(`Submodule is registered, updating URL from SSH to HTTPS...`, 'info');
|
||||
log(`Target URL: ${submoduleUrl}`, 'info');
|
||||
|
||||
// Update submodule URL using git submodule set-url (Git 2.25+)
|
||||
try {
|
||||
const setUrlResult = runCommand(
|
||||
`git submodule set-url packages/mobile-sdk-alpha/mobile-sdk-native "${submoduleUrl}"`,
|
||||
{ stdio: 'pipe' },
|
||||
REPO_ROOT,
|
||||
);
|
||||
log('Updated submodule URL using git submodule set-url', 'success');
|
||||
log(`Command result: ${setUrlResult}`, 'info');
|
||||
} catch (error) {
|
||||
log(`git submodule set-url failed: ${error.message}`, 'warning');
|
||||
// Fallback: Update .gitmodules file directly
|
||||
try {
|
||||
let gitmodulesContent = fs.readFileSync(gitmodulesPath, 'utf8');
|
||||
log(`Current .gitmodules content:\n${gitmodulesContent}`, 'info');
|
||||
// Replace the URL for mobile-sdk-native submodule
|
||||
const oldContent = gitmodulesContent;
|
||||
gitmodulesContent = gitmodulesContent.replace(
|
||||
/(\[submodule\s+"packages\/mobile-sdk-alpha\/mobile-sdk-native"\]\s+path\s*=\s*packages\/mobile-sdk-alpha\/mobile-sdk-native\s+url\s*=\s*)[^\s]+/,
|
||||
`$1${submoduleUrl}`,
|
||||
);
|
||||
if (oldContent !== gitmodulesContent) {
|
||||
fs.writeFileSync(gitmodulesPath, gitmodulesContent, 'utf8');
|
||||
log('Updated .gitmodules with new submodule URL', 'success');
|
||||
log(`New .gitmodules content:\n${gitmodulesContent}`, 'info');
|
||||
} else {
|
||||
log('No changes made to .gitmodules - regex may not match', 'warning');
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
log(`Could not update .gitmodules: ${fallbackError.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If directory exists but is empty, remove it so we can re-initialize
|
||||
if (submoduleExists && !submoduleHasContent) {
|
||||
log('Submodule directory exists but is empty, removing...', 'info');
|
||||
runCommand(`rm -rf "${path.relative(REPO_ROOT, PRIVATE_MODULE_PATH)}"`, { stdio: 'pipe' }, REPO_ROOT);
|
||||
}
|
||||
|
||||
if (isSubmoduleRegistered) {
|
||||
// Submodule is registered, update/init it
|
||||
log('Updating and initializing submodule...', 'info');
|
||||
try {
|
||||
const updateResult = runCommand(
|
||||
`git submodule update --init --recursive packages/mobile-sdk-alpha/mobile-sdk-native`,
|
||||
{},
|
||||
REPO_ROOT,
|
||||
);
|
||||
log(`Submodule update completed: ${updateResult}`, 'success');
|
||||
} catch (error) {
|
||||
log(`Submodule update failed: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// Add submodule
|
||||
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`;
|
||||
// Submodule not registered, add it
|
||||
log('Adding submodule...', 'info');
|
||||
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" packages/mobile-sdk-alpha/mobile-sdk-native`;
|
||||
if (isCI && (appToken || repoToken)) {
|
||||
// Security: Run command silently to avoid token exposure in logs
|
||||
runCommand(addCommand, { stdio: 'pipe' });
|
||||
runCommand(addCommand, { stdio: 'pipe' }, REPO_ROOT);
|
||||
} else {
|
||||
runCommand(addCommand);
|
||||
runCommand(addCommand, {}, REPO_ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,7 @@ interface PressableViewProps {
|
||||
}
|
||||
|
||||
export interface ViewProps
|
||||
extends Omit<RNViewProps, 'hitSlop'>,
|
||||
SpacingProps,
|
||||
Omit<ViewStyle, keyof SpacingProps>,
|
||||
PressableViewProps {}
|
||||
extends Omit<RNViewProps, 'hitSlop'>, SpacingProps, Omit<ViewStyle, keyof SpacingProps>, PressableViewProps {}
|
||||
|
||||
const sizeTokens: Record<string, number> = {
|
||||
$0: 0,
|
||||
|
||||
804
scripts/docstring-report.ts
Normal file
804
scripts/docstring-report.ts
Normal file
@@ -0,0 +1,804 @@
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import fs from 'node:fs/promises';
|
||||
import { glob } from 'node:fs/promises';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
interface CliOptions {
|
||||
patterns: string[];
|
||||
writeReport?: string;
|
||||
label?: string;
|
||||
includeDetails: boolean;
|
||||
}
|
||||
|
||||
interface ExportEntry {
|
||||
localName: string;
|
||||
kinds: Set<string>;
|
||||
exportedAs: Set<string>;
|
||||
documented: boolean;
|
||||
exported: boolean;
|
||||
}
|
||||
|
||||
interface FileExportSummary {
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
totalExports: number;
|
||||
documentedExports: number;
|
||||
coverage: number;
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
interface JsonReport {
|
||||
generatedAt: string;
|
||||
label?: string;
|
||||
totals: {
|
||||
exports: number;
|
||||
documented: number;
|
||||
undocumented: number;
|
||||
coverage: number;
|
||||
};
|
||||
undocumentedTotal: number;
|
||||
undocumentedSampled: number;
|
||||
undocumented: UndocumentedEntry[];
|
||||
files?: JsonReportFile[];
|
||||
}
|
||||
|
||||
interface JsonReportFile {
|
||||
file: string;
|
||||
exports: number;
|
||||
documented: number;
|
||||
undocumented: number;
|
||||
coverage: number;
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
interface UndocumentedEntry {
|
||||
file: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PATTERNS = [
|
||||
'app/src/**/*.{ts,tsx}',
|
||||
'packages/mobile-sdk-alpha/src/**/*.{ts,tsx}',
|
||||
];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const root = process.cwd();
|
||||
const files = await resolveFiles(options.patterns, root);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No source files matched the provided patterns.');
|
||||
if (options.writeReport) {
|
||||
await writeJsonReport(options.writeReport, {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: { exports: 0, documented: 0, undocumented: 0, coverage: 100 },
|
||||
undocumentedTotal: 0,
|
||||
undocumentedSampled: 0,
|
||||
undocumented: [],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const summaries: FileExportSummary[] = [];
|
||||
const failedFiles: Array<{ path: string; error: string }> = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const summary = await analyzeFile(filePath, root);
|
||||
if (summary.totalExports > 0) {
|
||||
summaries.push(summary);
|
||||
}
|
||||
} catch (error) {
|
||||
const relativePath = path.relative(root, filePath);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
failedFiles.push({ path: relativePath, error: errorMessage });
|
||||
console.error(`Failed to analyze ${relativePath}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaries.length === 0) {
|
||||
console.log('No exported declarations were found in the selected files.');
|
||||
if (options.writeReport) {
|
||||
await writeJsonReport(options.writeReport, {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: { exports: 0, documented: 0, undocumented: 0, coverage: 100 },
|
||||
undocumentedTotal: 0,
|
||||
undocumentedSampled: 0,
|
||||
undocumented: [],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
summaries.sort((a, b) => {
|
||||
if (a.coverage === b.coverage) {
|
||||
return a.relativePath.localeCompare(b.relativePath);
|
||||
}
|
||||
return a.coverage - b.coverage;
|
||||
});
|
||||
|
||||
const totalExports = summaries.reduce(
|
||||
(sum, file) => sum + file.totalExports,
|
||||
0,
|
||||
);
|
||||
const documentedExports = summaries.reduce(
|
||||
(sum, file) => sum + file.documentedExports,
|
||||
0,
|
||||
);
|
||||
const overallCoverage =
|
||||
totalExports === 0 ? 1 : documentedExports / totalExports;
|
||||
|
||||
printTable(summaries, options.label);
|
||||
printSummary(totalExports, documentedExports, overallCoverage);
|
||||
printUndocumentedHighlights(summaries);
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
console.log();
|
||||
console.log(`Failed to analyze ${failedFiles.length} file(s):`);
|
||||
for (const failure of failedFiles) {
|
||||
console.log(` ${failure.path}: ${failure.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.writeReport) {
|
||||
const missingEntries = summaries.flatMap(file =>
|
||||
file.missing.map<UndocumentedEntry>(symbol => ({
|
||||
file: file.relativePath,
|
||||
symbol,
|
||||
})),
|
||||
);
|
||||
const maxUndocumentedEntries = options.includeDetails
|
||||
? missingEntries.length
|
||||
: Math.min(50, missingEntries.length);
|
||||
const files = options.includeDetails
|
||||
? summaries
|
||||
.filter(file => file.missing.length > 0)
|
||||
.map<JsonReportFile>(file => ({
|
||||
file: file.relativePath,
|
||||
exports: file.totalExports,
|
||||
documented: file.documentedExports,
|
||||
undocumented: file.totalExports - file.documentedExports,
|
||||
coverage: Number((file.coverage * 100).toFixed(2)),
|
||||
missing: file.missing,
|
||||
}))
|
||||
: undefined;
|
||||
const report: JsonReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: {
|
||||
exports: totalExports,
|
||||
documented: documentedExports,
|
||||
undocumented: totalExports - documentedExports,
|
||||
coverage: Number((overallCoverage * 100).toFixed(2)),
|
||||
},
|
||||
undocumentedTotal: missingEntries.length,
|
||||
undocumentedSampled: maxUndocumentedEntries,
|
||||
undocumented: missingEntries.slice(0, maxUndocumentedEntries),
|
||||
...(files ? { files } : {}),
|
||||
};
|
||||
|
||||
await writeJsonReport(options.writeReport, report);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate docstring report.');
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): CliOptions {
|
||||
const patterns: string[] = [];
|
||||
let writeReport: string | undefined;
|
||||
let label: string | undefined;
|
||||
let includeDetails = false;
|
||||
|
||||
const expectValue = (flag: string, value: string | undefined): string => {
|
||||
if (!value) {
|
||||
throw new Error(`Missing value for ${flag}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg === '--write-report' || arg.startsWith('--write-report=')) {
|
||||
if (arg.includes('=')) {
|
||||
writeReport = arg.split('=')[1] ?? '';
|
||||
if (!writeReport) {
|
||||
throw new Error('Missing value for --write-report');
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
writeReport = expectValue('--write-report', args[index]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--label' || arg.startsWith('--label=')) {
|
||||
if (arg.includes('=')) {
|
||||
label = arg.split('=')[1] ?? '';
|
||||
} else {
|
||||
index += 1;
|
||||
label = expectValue('--label', args[index]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--')) {
|
||||
if (arg === '--details') {
|
||||
includeDetails = true;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
}
|
||||
|
||||
patterns.push(arg);
|
||||
}
|
||||
|
||||
if (patterns.length === 0) {
|
||||
patterns.push(...DEFAULT_PATTERNS);
|
||||
}
|
||||
|
||||
return { patterns, writeReport, label, includeDetails };
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
const usage = `Usage: docstring-report [pattern ...] [--write-report <path>] [--label <name>] [--details]
|
||||
|
||||
Examples:
|
||||
yarn tsx scripts/docstring-report.ts
|
||||
yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\"
|
||||
yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\" --label \"Mobile App\" --write-report docs/coverage/app.json --details`;
|
||||
console.log(usage);
|
||||
}
|
||||
|
||||
async function resolveFiles(
|
||||
patterns: string[],
|
||||
root: string,
|
||||
): Promise<string[]> {
|
||||
const files = new Set<string>();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for await (const match of glob(pattern, {
|
||||
cwd: root,
|
||||
// Exclude dotfiles and dot-directories
|
||||
exclude: (name: string) => path.basename(name).startsWith('.'),
|
||||
})) {
|
||||
const resolved = path.resolve(root, String(match));
|
||||
|
||||
// Skip directories (glob may return them despite file extension patterns)
|
||||
try {
|
||||
const stat = await fs.stat(resolved);
|
||||
if (stat.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or can't be accessed, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldIncludeFile(resolved, root)) {
|
||||
files.add(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(files).sort();
|
||||
}
|
||||
|
||||
function shouldIncludeFile(filePath: string, root: string): boolean {
|
||||
const relative = path.relative(root, filePath).replace(/\\/g, '/');
|
||||
|
||||
if (relative.endsWith('.d.ts') || relative.endsWith('.d.tsx')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\.test\.[tj]sx?$/.test(relative) || /\.spec\.[tj]sx?$/.test(relative)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\.stories\.[tj]sx?$/.test(relative)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (relative.includes('/__tests__/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function analyzeFile(
|
||||
filePath: string,
|
||||
root: string,
|
||||
): Promise<FileExportSummary> {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const scriptKind = filePath.endsWith('.tsx')
|
||||
? ts.ScriptKind.TSX
|
||||
: ts.ScriptKind.TS;
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
|
||||
const entries = new Map<string, ExportEntry>();
|
||||
const exportSpecifiers: Array<{ localName: string; exportedAs: string }> = [];
|
||||
const exportDefaultStatements: ts.ExportAssignment[] = [];
|
||||
const exportedDeclarations: Array<{
|
||||
statement: ts.Statement;
|
||||
hasDefault: boolean;
|
||||
}> = [];
|
||||
|
||||
// First pass: Collect all declarations with their documentation status
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isExportDeclaration(statement)) {
|
||||
// Collect export specifiers for second pass
|
||||
if (
|
||||
!statement.moduleSpecifier &&
|
||||
statement.exportClause &&
|
||||
ts.isNamedExports(statement.exportClause)
|
||||
) {
|
||||
for (const element of statement.exportClause.elements) {
|
||||
const localName = element.propertyName
|
||||
? element.propertyName.text
|
||||
: element.name.text;
|
||||
const exportedAs = element.name.text;
|
||||
exportSpecifiers.push({ localName, exportedAs });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ts.isExportAssignment(statement)) {
|
||||
if (!statement.isExportEquals) {
|
||||
exportDefaultStatements.push(statement);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
const exported = hasExportModifier(statement.modifiers);
|
||||
const statementDoc = hasDocComment(statement, sourceFile);
|
||||
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
// Extract all binding identifiers (handles destructuring)
|
||||
const identifiers = getBindingIdentifiers(declaration);
|
||||
if (identifiers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const declarationDoc = hasDocComment(declaration, sourceFile);
|
||||
|
||||
for (const name of identifiers) {
|
||||
const entry = ensureEntry(entries, name);
|
||||
entry.kinds.add('variable');
|
||||
entry.documented ||= statementDoc || declarationDoc;
|
||||
}
|
||||
|
||||
if (exported) {
|
||||
exportedDeclarations.push({ statement, hasDefault: false });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isFunctionDeclaration(statement) ||
|
||||
ts.isClassDeclaration(statement) ||
|
||||
ts.isInterfaceDeclaration(statement) ||
|
||||
ts.isTypeAliasDeclaration(statement) ||
|
||||
ts.isEnumDeclaration(statement) ||
|
||||
ts.isModuleDeclaration(statement)
|
||||
) {
|
||||
const name = getDeclarationName(statement, sourceFile);
|
||||
const hasExport = hasExportModifier(statement.modifiers);
|
||||
const hasDefault = hasDefaultModifier(statement.modifiers);
|
||||
|
||||
// For anonymous default exports (e.g., export default function() {}),
|
||||
// use "default" as the name so they're tracked in coverage
|
||||
const effectiveName = !name && hasExport && hasDefault ? 'default' : name;
|
||||
|
||||
if (!effectiveName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = ensureEntry(entries, effectiveName);
|
||||
entry.kinds.add(getKindLabel(statement));
|
||||
entry.documented ||= hasDocComment(statement, sourceFile);
|
||||
|
||||
if (hasExport) {
|
||||
exportedDeclarations.push({ statement, hasDefault });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Process all exports now that all declarations are collected
|
||||
// Process inline exported declarations
|
||||
for (const { statement, hasDefault } of exportedDeclarations) {
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
// Extract all binding identifiers (handles destructuring)
|
||||
const identifiers = getBindingIdentifiers(declaration);
|
||||
|
||||
for (const name of identifiers) {
|
||||
const entry = entries.get(name);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
entry.exportedAs.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const name = getDeclarationName(statement, sourceFile);
|
||||
|
||||
// For anonymous default exports, use "default" as the name
|
||||
const effectiveName = !name && hasDefault ? 'default' : name;
|
||||
|
||||
if (!effectiveName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = entries.get(effectiveName);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
// For inline default exports (export default function foo), add "default" not the name
|
||||
const exportName = hasDefault ? 'default' : effectiveName;
|
||||
entry.exportedAs.add(exportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process export specifiers (export { Foo, Bar })
|
||||
for (const specifier of exportSpecifiers) {
|
||||
const entry = entries.get(specifier.localName);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
entry.exportedAs.add(specifier.exportedAs);
|
||||
}
|
||||
}
|
||||
|
||||
// Process export default statements (export default Foo)
|
||||
for (const statement of exportDefaultStatements) {
|
||||
const entry = ensureEntry(entries, 'default');
|
||||
entry.exported = true;
|
||||
entry.kinds.add('default');
|
||||
entry.exportedAs.add('default');
|
||||
|
||||
// Check if the export statement itself is documented
|
||||
entry.documented ||= hasDocComment(statement, sourceFile);
|
||||
|
||||
// If exporting an identifier (export default Foo), inherit documentation from the referenced declaration
|
||||
if (ts.isIdentifier(statement.expression)) {
|
||||
const referencedName = statement.expression.text;
|
||||
const referencedEntry = entries.get(referencedName);
|
||||
if (referencedEntry?.documented) {
|
||||
entry.documented = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relativePath = path.relative(root, filePath).replace(/\\/g, '/');
|
||||
const exportedEntries = Array.from(entries.values()).filter(
|
||||
entry => entry.exported,
|
||||
);
|
||||
const documentedEntries = exportedEntries.filter(entry => entry.documented);
|
||||
|
||||
const missing = exportedEntries
|
||||
.filter(entry => !entry.documented)
|
||||
.map(entry => formatMissingName(entry));
|
||||
|
||||
return {
|
||||
filePath,
|
||||
relativePath,
|
||||
totalExports: exportedEntries.length,
|
||||
documentedExports: documentedEntries.length,
|
||||
coverage:
|
||||
exportedEntries.length === 0
|
||||
? 1
|
||||
: documentedEntries.length / exportedEntries.length,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureEntry(map: Map<string, ExportEntry>, key: string): ExportEntry {
|
||||
const existing = map.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const entry: ExportEntry = {
|
||||
localName: key,
|
||||
kinds: new Set<string>(),
|
||||
exportedAs: new Set<string>(),
|
||||
documented: false,
|
||||
exported: false,
|
||||
};
|
||||
|
||||
map.set(key, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function hasExportModifier(
|
||||
modifiers: ts.NodeArray<ts.Modifier> | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
modifiers?.some(
|
||||
modifier =>
|
||||
modifier.kind === ts.SyntaxKind.ExportKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.DefaultKeyword,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hasDefaultModifier(
|
||||
modifiers: ts.NodeArray<ts.Modifier> | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword),
|
||||
);
|
||||
}
|
||||
|
||||
function getDeclarationName(
|
||||
node: ts.Node,
|
||||
sourceFile: ts.SourceFile,
|
||||
): string | undefined {
|
||||
if ('name' in node && node.name) {
|
||||
const nameNode = (node as ts.Node & { name?: ts.Node }).name as
|
||||
| ts.Node
|
||||
| undefined;
|
||||
if (!nameNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isIdentifier(nameNode) ||
|
||||
ts.isStringLiteralLike(nameNode) ||
|
||||
ts.isNumericLiteral(nameNode)
|
||||
) {
|
||||
return nameNode.text;
|
||||
}
|
||||
|
||||
return nameNode.getText(sourceFile).trim();
|
||||
}
|
||||
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
return node.name.text;
|
||||
}
|
||||
|
||||
if (ts.isExportAssignment(node)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all binding identifiers from a declaration.
|
||||
* Handles destructuring patterns like { a, b } and [x, y].
|
||||
*/
|
||||
function getBindingIdentifiers(declaration: ts.VariableDeclaration): string[] {
|
||||
const identifiers: string[] = [];
|
||||
|
||||
function collectIdentifiers(name: ts.BindingName): void {
|
||||
if (ts.isIdentifier(name)) {
|
||||
identifiers.push(name.text);
|
||||
} else if (ts.isObjectBindingPattern(name)) {
|
||||
for (const element of name.elements) {
|
||||
collectIdentifiers(element.name);
|
||||
}
|
||||
} else if (ts.isArrayBindingPattern(name)) {
|
||||
for (const element of name.elements) {
|
||||
if (ts.isBindingElement(element)) {
|
||||
collectIdentifiers(element.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectIdentifiers(declaration.name);
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
function getKindLabel(node: ts.Node): string {
|
||||
if (ts.isFunctionDeclaration(node)) {
|
||||
return 'function';
|
||||
}
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
return 'class';
|
||||
}
|
||||
if (ts.isInterfaceDeclaration(node)) {
|
||||
return 'interface';
|
||||
}
|
||||
if (ts.isTypeAliasDeclaration(node)) {
|
||||
return 'type';
|
||||
}
|
||||
if (ts.isEnumDeclaration(node)) {
|
||||
return 'enum';
|
||||
}
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
return 'namespace';
|
||||
}
|
||||
return 'declaration';
|
||||
}
|
||||
|
||||
function hasDocComment(node: ts.Node, sourceFile: ts.SourceFile): boolean {
|
||||
const jsDocNodes = (node as ts.Node & { jsDoc?: readonly ts.JSDoc[] }).jsDoc;
|
||||
if (jsDocNodes && jsDocNodes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jsDocRanges = ts.getJSDocCommentRanges(node, sourceFile.text);
|
||||
if (jsDocRanges && jsDocRanges.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const leadingRanges = ts.getLeadingCommentRanges(
|
||||
sourceFile.text,
|
||||
node.getFullStart(),
|
||||
);
|
||||
if (leadingRanges) {
|
||||
return leadingRanges.some(range =>
|
||||
sourceFile.text.slice(range.pos, range.end).startsWith('/**'),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
return `${(value * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function printTable(summaries: FileExportSummary[], label?: string): void {
|
||||
const title = label ? `Docstring coverage (${label})` : 'Docstring coverage';
|
||||
console.log(title);
|
||||
console.log('='.repeat(title.length));
|
||||
|
||||
const headers = ['File', 'Exports', 'With Docs', 'Coverage', 'Missing'];
|
||||
const rows = summaries.map(summary => [
|
||||
summary.relativePath,
|
||||
summary.totalExports.toString(),
|
||||
summary.documentedExports.toString(),
|
||||
formatPercent(summary.coverage),
|
||||
summary.missing.join(', '),
|
||||
]);
|
||||
|
||||
const widths = headers.map((header, columnIndex) => {
|
||||
const columnValues = rows.map(row => row[columnIndex]);
|
||||
const maxContentLength = columnValues.reduce(
|
||||
(max, value) => Math.max(max, value.length),
|
||||
header.length,
|
||||
);
|
||||
const maxWidth =
|
||||
columnIndex === 0
|
||||
? Math.min(70, Math.max(20, maxContentLength))
|
||||
: maxContentLength;
|
||||
return maxWidth;
|
||||
});
|
||||
|
||||
const formatRow = (values: string[]): string =>
|
||||
values
|
||||
.map((value, index) => {
|
||||
const width = widths[index];
|
||||
const trimmed =
|
||||
index === 0 && value.length > width
|
||||
? `…${value.slice(value.length - width + 1)}`
|
||||
: value;
|
||||
return trimmed.padEnd(width, ' ');
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
console.log(formatRow(headers));
|
||||
console.log(
|
||||
formatRow(
|
||||
widths.map(width => '-'.repeat(Math.max(3, Math.min(width, 80)))),
|
||||
),
|
||||
);
|
||||
rows.forEach(row => console.log(formatRow(row)));
|
||||
}
|
||||
|
||||
function printSummary(
|
||||
total: number,
|
||||
documented: number,
|
||||
coverage: number,
|
||||
): void {
|
||||
console.log();
|
||||
if (total === 0) {
|
||||
console.log('Overall coverage: 100.00% (0/0 exported declarations)');
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`Overall coverage: ${formatPercent(coverage)} (${documented}/${total} exported declarations documented)`,
|
||||
);
|
||||
}
|
||||
|
||||
function printUndocumentedHighlights(summaries: FileExportSummary[]): void {
|
||||
const missingEntries: Array<{ file: string; names: string[] }> = [];
|
||||
for (const summary of summaries) {
|
||||
if (summary.missing.length > 0) {
|
||||
missingEntries.push({
|
||||
file: summary.relativePath,
|
||||
names: summary.missing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (missingEntries.length === 0) {
|
||||
console.log('All exported declarations include TSDoc comments.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('Undocumented exports:');
|
||||
for (const entry of missingEntries) {
|
||||
console.log(` ${entry.file}`);
|
||||
for (const name of entry.names) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatMissingName(entry: ExportEntry): string {
|
||||
const exportedNames = Array.from(entry.exportedAs);
|
||||
if (exportedNames.length === 0) {
|
||||
return entry.localName;
|
||||
}
|
||||
|
||||
const aliasList = exportedNames.filter(
|
||||
name => name !== entry.localName && name !== 'default',
|
||||
);
|
||||
if (exportedNames.includes('default')) {
|
||||
if (aliasList.length > 0) {
|
||||
return `default (local: ${entry.localName}, aliases: ${aliasList.join(', ')})`;
|
||||
}
|
||||
if (entry.localName !== 'default') {
|
||||
return `default (local: ${entry.localName})`;
|
||||
}
|
||||
return 'default export';
|
||||
}
|
||||
|
||||
if (aliasList.length > 0) {
|
||||
return `${aliasList.join(', ')} (local: ${entry.localName})`;
|
||||
}
|
||||
|
||||
return entry.localName;
|
||||
}
|
||||
|
||||
async function writeJsonReport(
|
||||
targetPath: string,
|
||||
report: JsonReport,
|
||||
): Promise<void> {
|
||||
const resolvedPath = path.resolve(process.cwd(), targetPath);
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
resolvedPath,
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
console.log(
|
||||
`\nSaved coverage snapshot to ${path.relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void main();
|
||||
Reference in New Issue
Block a user