mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -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
|
# Development & Testing
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
# Test coverage
|
# Test coverage (but allow docs/coverage for docstring reports)
|
||||||
**/coverage/
|
**/coverage/
|
||||||
|
!docs/coverage/
|
||||||
**/.nyc_output/
|
**/.nyc_output/
|
||||||
|
|
||||||
# Test files (optional - you might want AI to see tests)
|
# Test files (optional - you might want AI to see tests)
|
||||||
@@ -261,6 +262,9 @@ circuits/ptau/
|
|||||||
!metro.config.*
|
!metro.config.*
|
||||||
!tamagui.config.ts
|
!tamagui.config.ts
|
||||||
|
|
||||||
|
# Allow docstring coverage reports (tracked in git for coverage tracking)
|
||||||
|
!docs/coverage/*.json
|
||||||
|
|
||||||
# Ensure source code is accessible
|
# Ensure source code is accessible
|
||||||
!**/*.ts
|
!**/*.ts
|
||||||
!**/*.tsx
|
!**/*.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
|
# Configure Yarn
|
||||||
corepack enable
|
corepack enable
|
||||||
yarn set version 4.6.0
|
yarn set version 4.12.0
|
||||||
|
|
||||||
echo "📦 Installing JavaScript dependencies with strict lock file..."
|
echo "📦 Installing JavaScript dependencies with strict lock file..."
|
||||||
if ! yarn install --immutable --inline-builds; then
|
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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.6.0 --activate
|
corepack prepare yarn@4.12.0 --activate
|
||||||
# Ensure we're using the correct version
|
# Ensure we're using the correct version
|
||||||
yarn --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
|
- name: Setup Corepack
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.6.0 --activate
|
corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
id: build-cache
|
id: build-cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- name: Setup Corepack
|
- name: Setup Corepack
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.6.0 --activate
|
corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
id: build-cache
|
id: build-cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
- name: Setup Corepack
|
- name: Setup Corepack
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.6.0 --activate
|
corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
id: build-cache
|
id: build-cache
|
||||||
uses: actions/cache/restore@v4
|
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"
|
NODE_ENV: "production"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "app/**"
|
||||||
|
- "packages/mobile-sdk-alpha/**"
|
||||||
|
- ".github/workflows/mobile-bundle-analysis.yml"
|
||||||
|
- ".github/actions/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "app/**"
|
- "app/**"
|
||||||
- "packages/mobile-sdk-alpha/**"
|
- "packages/mobile-sdk-alpha/**"
|
||||||
@@ -18,7 +32,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze-android:
|
analyze-android:
|
||||||
runs-on: macos-latest-large
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: Read and sanitize Node.js version
|
- name: Read and sanitize Node.js version
|
||||||
@@ -83,7 +97,9 @@ jobs:
|
|||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|
||||||
analyze-ios:
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: Read and sanitize Node.js version
|
- name: Read and sanitize Node.js version
|
||||||
@@ -122,6 +138,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: app/ios/Pods
|
path: app/ios/Pods
|
||||||
lockfile: app/ios/Podfile.lock
|
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
|
- name: Generate token for self repositories
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: ./.github/actions/generate-github-token
|
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 }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Activate Yarn 4.6.0
|
- name: Activate Yarn 4.12.0
|
||||||
run: corepack prepare yarn@4.6.0 --activate
|
run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Cache Yarn
|
- name: Cache Yarn
|
||||||
uses: ./.github/actions/cache-yarn
|
uses: ./.github/actions/cache-yarn
|
||||||
with:
|
with:
|
||||||
@@ -116,8 +116,8 @@ jobs:
|
|||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Activate Yarn 4.6.0
|
- name: Activate Yarn 4.12.0
|
||||||
run: corepack prepare yarn@4.6.0 --activate
|
run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Cache Yarn
|
- name: Cache Yarn
|
||||||
uses: ./.github/actions/cache-yarn
|
uses: ./.github/actions/cache-yarn
|
||||||
with:
|
with:
|
||||||
@@ -203,7 +203,10 @@ jobs:
|
|||||||
yarn test:ci
|
yarn test:ci
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
build-ios:
|
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
|
needs: build-deps
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
@@ -231,8 +234,8 @@ jobs:
|
|||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Activate Yarn 4.6.0
|
- name: Activate Yarn 4.12.0
|
||||||
run: corepack prepare yarn@4.6.0 --activate
|
run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Set up Xcode
|
- name: Set up Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
@@ -251,6 +254,21 @@ jobs:
|
|||||||
echo "Xcode path:"
|
echo "Xcode path:"
|
||||||
xcode-select -p
|
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
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
@@ -267,8 +285,7 @@ jobs:
|
|||||||
- name: Cache Ruby gems
|
- name: Cache Ruby gems
|
||||||
uses: ./.github/actions/cache-bundler
|
uses: ./.github/actions/cache-bundler
|
||||||
with:
|
with:
|
||||||
# TODO(jcortejoso): Confirm the path of the bundle cache
|
path: app/vendor/bundle
|
||||||
path: app/ios/vendor/bundle
|
|
||||||
lock-file: app/Gemfile.lock
|
lock-file: app/Gemfile.lock
|
||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||||
- name: Cache Pods
|
- name: Cache Pods
|
||||||
@@ -297,6 +314,14 @@ jobs:
|
|||||||
key: ${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock') }}
|
key: ${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-${{ hashFiles('app/ios/Podfile.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-xcode-index-${{ env.XCODE_VERSION }}-
|
${{ 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
|
- name: Install Mobile Dependencies
|
||||||
uses: ./.github/actions/yarn-install
|
uses: ./.github/actions/yarn-install
|
||||||
- name: Cache Built Dependencies
|
- name: Cache Built Dependencies
|
||||||
@@ -306,6 +331,8 @@ jobs:
|
|||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }}
|
||||||
- name: Build dependencies (cache miss)
|
- name: Build dependencies (cache miss)
|
||||||
# if: steps.built-deps.outputs.cache-hit != 'true'
|
# if: steps.built-deps.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token || '' }}
|
||||||
run: |
|
run: |
|
||||||
echo "Cache miss for built dependencies. Building now..."
|
echo "Cache miss for built dependencies. Building now..."
|
||||||
yarn workspace @selfxyz/mobile-app run build:deps
|
yarn workspace @selfxyz/mobile-app run build:deps
|
||||||
@@ -315,14 +342,6 @@ jobs:
|
|||||||
bundle config set --local path 'vendor/bundle'
|
bundle config set --local path 'vendor/bundle'
|
||||||
bundle install --jobs 4 --retry 3
|
bundle install --jobs 4 --retry 3
|
||||||
working-directory: ./app
|
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
|
- name: Install iOS Dependencies
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
@@ -405,6 +424,8 @@ jobs:
|
|||||||
build-android:
|
build-android:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-deps
|
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
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -427,8 +448,8 @@ jobs:
|
|||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Activate Yarn 4.6.0
|
- name: Activate Yarn 4.12.0
|
||||||
run: corepack prepare yarn@4.6.0 --activate
|
run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Cache Yarn
|
- name: Cache Yarn
|
||||||
uses: ./.github/actions/cache-yarn
|
uses: ./.github/actions/cache-yarn
|
||||||
with:
|
with:
|
||||||
|
|||||||
18
.github/workflows/mobile-deploy.yml
vendored
18
.github/workflows/mobile-deploy.yml
vendored
@@ -265,7 +265,8 @@ jobs:
|
|||||||
|
|
||||||
build-ios:
|
build-ios:
|
||||||
needs: [bump-version]
|
needs: [bump-version]
|
||||||
runs-on: macos-latest-large
|
# runs-on: macos-latest-large
|
||||||
|
runs-on: namespace-profile-apple-silicon-6cpu
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: write
|
actions: write
|
||||||
@@ -430,6 +431,21 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Lock files exist"
|
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
|
- name: Generate token for self repositories
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: ./.github/actions/generate-github-token
|
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
|
MAESTRO_VERSION: 1.41.0
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "app/**"
|
||||||
|
- "packages/mobile-sdk-alpha/**"
|
||||||
|
- ".github/workflows/mobile-e2e.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
@@ -26,6 +35,7 @@ on:
|
|||||||
- "app/**"
|
- "app/**"
|
||||||
- "packages/mobile-sdk-alpha/**"
|
- "packages/mobile-sdk-alpha/**"
|
||||||
- ".github/workflows/mobile-e2e.yml"
|
- ".github/workflows/mobile-e2e.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
android-build-test:
|
android-build-test:
|
||||||
@@ -55,7 +65,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Compute .yarnrc.yml hash
|
- name: Compute .yarnrc.yml hash
|
||||||
id: yarnrc-hash
|
id: yarnrc-hash
|
||||||
uses: ./.github/actions/yarnrc-hash
|
uses: ./.github/actions/yarnrc-hash
|
||||||
@@ -136,9 +146,9 @@ jobs:
|
|||||||
- name: Build dependencies (outside emulator)
|
- name: Build dependencies (outside emulator)
|
||||||
run: |
|
run: |
|
||||||
echo "Building dependencies..."
|
echo "Building dependencies..."
|
||||||
# Ensure Yarn 4.6.0 is active
|
# Ensure Yarn 4.12.0 is active
|
||||||
corepack enable
|
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; }
|
yarn workspace @selfxyz/mobile-app run build:deps || { echo "❌ Dependency build failed"; exit 1; }
|
||||||
echo "✅ Dependencies built successfully"
|
echo "✅ Dependencies built successfully"
|
||||||
- name: Setup Android private modules
|
- name: Setup Android private modules
|
||||||
@@ -229,7 +239,8 @@ jobs:
|
|||||||
|
|
||||||
e2e-ios:
|
e2e-ios:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
runs-on: macos-latest-large
|
# runs-on: macos-latest-large
|
||||||
|
runs-on: namespace-profile-apple-silicon-6cpu
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -258,7 +269,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Compute .yarnrc.yml hash
|
- name: Compute .yarnrc.yml hash
|
||||||
id: yarnrc-hash
|
id: yarnrc-hash
|
||||||
uses: ./.github/actions/yarnrc-hash
|
uses: ./.github/actions/yarnrc-hash
|
||||||
@@ -273,6 +284,21 @@ jobs:
|
|||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
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
|
- name: Generate token for self repositories
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: ./.github/actions/generate-github-token
|
uses: ./.github/actions/generate-github-token
|
||||||
@@ -360,7 +386,19 @@ jobs:
|
|||||||
- name: Verify iOS Runtime
|
- name: Verify iOS Runtime
|
||||||
run: |
|
run: |
|
||||||
echo "📱 Verifying iOS Runtime availability..."
|
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
|
xcrun simctl list runtimes | grep iOS
|
||||||
- name: Build dependencies (outside main flow)
|
- name: Build dependencies (outside main flow)
|
||||||
run: |
|
run: |
|
||||||
@@ -383,6 +421,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Setting up iOS Simulator..."
|
echo "Setting up iOS Simulator..."
|
||||||
|
|
||||||
|
# Ensure simulator directories exist
|
||||||
|
mkdir -p "$HOME/Library/Developer/CoreSimulator/Devices"
|
||||||
|
|
||||||
# First, check what simulators are actually available
|
# First, check what simulators are actually available
|
||||||
echo "Available simulators:"
|
echo "Available simulators:"
|
||||||
xcrun simctl list devices available || {
|
xcrun simctl list devices available || {
|
||||||
@@ -527,12 +568,16 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Verifying app installation..."
|
echo "Verifying app installation..."
|
||||||
if xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1; then
|
# get_app_container may fail with NSPOSIXErrorDomain if app isn't ready yet - handle gracefully
|
||||||
echo "✅ App successfully installed"
|
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
|
else
|
||||||
echo "❌ App installation verification failed"
|
echo "⚠️ App installation verification returned exit code ${APP_CONTAINER_EXIT:-unknown} (may be expected)"
|
||||||
exit 1
|
# 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
|
fi
|
||||||
|
unset APP_CONTAINER_OUTPUT APP_CONTAINER_EXIT
|
||||||
|
|
||||||
echo "🚀 Testing app launch capability..."
|
echo "🚀 Testing app launch capability..."
|
||||||
xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID" || {
|
xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID" || {
|
||||||
@@ -541,15 +586,46 @@ jobs:
|
|||||||
|
|
||||||
echo "⏰ Checking simulator readiness..."
|
echo "⏰ Checking simulator readiness..."
|
||||||
sleep 10
|
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
|
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..."
|
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
|
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
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
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
|
E2E_TESTING: 1
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "packages/mobile-sdk-demo/**"
|
||||||
|
- "packages/mobile-sdk-alpha/**"
|
||||||
|
- ".github/workflows/mobile-sdk-demo-e2e.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
@@ -28,6 +37,7 @@ on:
|
|||||||
- "packages/mobile-sdk-demo/**"
|
- "packages/mobile-sdk-demo/**"
|
||||||
- "packages/mobile-sdk-alpha/**"
|
- "packages/mobile-sdk-alpha/**"
|
||||||
- ".github/workflows/mobile-sdk-demo-e2e.yml"
|
- ".github/workflows/mobile-sdk-demo-e2e.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
android-e2e:
|
android-e2e:
|
||||||
@@ -58,7 +68,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Compute .yarnrc.yml hash
|
- name: Compute .yarnrc.yml hash
|
||||||
id: yarnrc-hash
|
id: yarnrc-hash
|
||||||
uses: ./.github/actions/yarnrc-hash
|
uses: ./.github/actions/yarnrc-hash
|
||||||
@@ -202,7 +212,11 @@ jobs:
|
|||||||
|
|
||||||
ios-e2e:
|
ios-e2e:
|
||||||
timeout-minutes: 60
|
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
|
name: iOS E2E Tests Demo App
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
||||||
@@ -229,7 +243,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.12.0 --activate
|
||||||
- name: Compute .yarnrc.yml hash
|
- name: Compute .yarnrc.yml hash
|
||||||
id: yarnrc-hash
|
id: yarnrc-hash
|
||||||
uses: ./.github/actions/yarnrc-hash
|
uses: ./.github/actions/yarnrc-hash
|
||||||
@@ -244,6 +258,21 @@ jobs:
|
|||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
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
|
- name: Generate token for self repositories
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: ./.github/actions/generate-github-token
|
uses: ./.github/actions/generate-github-token
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1194.0)
|
aws-partitions (1.1198.0)
|
||||||
aws-sdk-core (3.239.2)
|
aws-sdk-core (3.240.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
@@ -34,7 +34,7 @@ GEM
|
|||||||
aws-sdk-kms (1.118.0)
|
aws-sdk-kms (1.118.0)
|
||||||
aws-sdk-core (~> 3, >= 3.239.1)
|
aws-sdk-core (~> 3, >= 3.239.1)
|
||||||
aws-sigv4 (~> 1.5)
|
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-core (~> 3, >= 3.234.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
@@ -43,7 +43,7 @@ GEM
|
|||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
benchmark (0.5.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (3.3.1)
|
bigdecimal (4.0.1)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
cocoapods (1.16.2)
|
cocoapods (1.16.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
@@ -118,7 +118,7 @@ GEM
|
|||||||
faraday-em_synchrony (1.0.1)
|
faraday-em_synchrony (1.0.1)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.1.1)
|
faraday-multipart (1.2.0)
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
@@ -219,7 +219,7 @@ GEM
|
|||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
i18n (1.14.7)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.18.0)
|
json (2.18.0)
|
||||||
@@ -229,7 +229,8 @@ GEM
|
|||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.27.0)
|
minitest (6.0.0)
|
||||||
|
prism (~> 1.5)
|
||||||
molinillo (0.8.0)
|
molinillo (0.8.0)
|
||||||
multi_json (1.18.0)
|
multi_json (1.18.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
@@ -244,6 +245,7 @@ GEM
|
|||||||
optparse (0.8.1)
|
optparse (0.8.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
|
prism (1.7.0)
|
||||||
public_suffix (4.0.7)
|
public_suffix (4.0.7)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ android {
|
|||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 121
|
versionCode 121
|
||||||
versionName "2.9.5"
|
versionName "2.9.7"
|
||||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
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
|
#### `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)
|
1. Sets up the environment (Node.js, Ruby, CocoaPods)
|
||||||
2. Processes iOS secrets and certificates
|
2. Processes iOS secrets and certificates
|
||||||
3. Runs appropriate Fastlane lane based on branch
|
3. Runs appropriate Fastlane lane based on branch
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.9.5</string>
|
<string>2.9.7</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@@ -2131,7 +2131,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Sentry/HybridSDK (= 8.53.2)
|
- Sentry/HybridSDK (= 8.53.2)
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNSVG (15.15.0):
|
- RNSVG (15.14.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
@@ -2151,9 +2151,9 @@ PODS:
|
|||||||
- ReactCodegen
|
- ReactCodegen
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- RNSVG/common (= 15.15.0)
|
- RNSVG/common (= 15.14.0)
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNSVG/common (15.15.0):
|
- RNSVG/common (15.14.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
@@ -2635,7 +2635,7 @@ SPEC CHECKSUMS:
|
|||||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||||
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
|
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||||
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
|
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
|
||||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
|
|||||||
@@ -546,7 +546,7 @@
|
|||||||
"$(PROJECT_DIR)",
|
"$(PROJECT_DIR)",
|
||||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.9.5;
|
MARKETING_VERSION = 2.9.7;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -686,7 +686,7 @@
|
|||||||
"$(PROJECT_DIR)",
|
"$(PROJECT_DIR)",
|
||||||
"$(PROJECT_DIR)/MoproKit/Libs",
|
"$(PROJECT_DIR)/MoproKit/Libs",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.9.5;
|
MARKETING_VERSION = 2.9.7;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ const config = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
transformer: {
|
transformer: {
|
||||||
babelTransformerPath: require.resolve(
|
babelTransformerPath:
|
||||||
'react-native-svg-transformer/react-native',
|
require.resolve('react-native-svg-transformer/react-native'),
|
||||||
),
|
|
||||||
disableImportExportTransform: true,
|
disableImportExportTransform: true,
|
||||||
inlineRequires: true,
|
inlineRequires: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@selfxyz/mobile-app",
|
"name": "@selfxyz/mobile-app",
|
||||||
"version": "2.9.5",
|
"version": "2.9.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -162,8 +162,8 @@
|
|||||||
"react-native-safe-area-context": "^5.6.1",
|
"react-native-safe-area-context": "^5.6.1",
|
||||||
"react-native-screens": "4.15.3",
|
"react-native-screens": "4.15.3",
|
||||||
"react-native-sqlite-storage": "^6.0.1",
|
"react-native-sqlite-storage": "^6.0.1",
|
||||||
"react-native-svg": "^15.14.0",
|
"react-native-svg": "15.14.0",
|
||||||
"react-native-svg-web": "^1.0.9",
|
"react-native-svg-web": "1.0.9",
|
||||||
"react-native-url-polyfill": "^3.0.0",
|
"react-native-url-polyfill": "^3.0.0",
|
||||||
"react-native-web": "^0.19.0",
|
"react-native-web": "^0.19.0",
|
||||||
"react-native-webview": "^13.16.0",
|
"react-native-webview": "^13.16.0",
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-svgr": "^4.5.0"
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22 <23"
|
"node": ">=22 <23"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ const ModalBackDrop = styled(View, {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ModalNavigationParams
|
export interface ModalNavigationParams extends Omit<
|
||||||
extends Omit<ModalParams, 'onButtonPress' | 'onModalDismiss'> {
|
ModalParams,
|
||||||
|
'onButtonPress' | 'onModalDismiss'
|
||||||
|
> {
|
||||||
callbackId: number;
|
callbackId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
|||||||
const navigation =
|
const navigation =
|
||||||
useNavigation() as NativeStackScreenProps<RootStackParamList>['navigation'];
|
useNavigation() as NativeStackScreenProps<RootStackParamList>['navigation'];
|
||||||
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
|
||||||
|
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
|
||||||
|
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
|
||||||
const [hasNotificationPermission, setHasNotificationPermission] =
|
const [hasNotificationPermission, setHasNotificationPermission] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const paddingBottom = useSafeBottomPadding(20);
|
const paddingBottom = useSafeBottomPadding(20);
|
||||||
@@ -668,6 +670,45 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
|||||||
</YStack>
|
</YStack>
|
||||||
</ParameterSection>
|
</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
|
<ParameterSection
|
||||||
icon={<WarningIcon color={yellow500} />}
|
icon={<WarningIcon color={yellow500} />}
|
||||||
title="Danger Zone"
|
title="Danger Zone"
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ import {
|
|||||||
setDefaultDocumentTypeIfNeeded,
|
setDefaultDocumentTypeIfNeeded,
|
||||||
usePassport,
|
usePassport,
|
||||||
} from '@/providers/passportDataProvider';
|
} from '@/providers/passportDataProvider';
|
||||||
import { getPointsAddress } from '@/services/points';
|
import {
|
||||||
|
getPointsAddress,
|
||||||
|
getWhiteListedDisclosureAddresses,
|
||||||
|
} from '@/services/points';
|
||||||
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
||||||
import { ProofStatus } from '@/stores/proofTypes';
|
import { ProofStatus } from '@/stores/proofTypes';
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +67,7 @@ const ProveScreen: React.FC = () => {
|
|||||||
const { useProvingStore, useSelfAppStore } = selfClient;
|
const { useProvingStore, useSelfAppStore } = selfClient;
|
||||||
const selectedApp = useSelfAppStore(state => state.selfApp);
|
const selectedApp = useSelfAppStore(state => state.selfApp);
|
||||||
const selectedAppRef = useRef<typeof selectedApp>(null);
|
const selectedAppRef = useRef<typeof selectedApp>(null);
|
||||||
|
const processedSessionsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false);
|
const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false);
|
||||||
const [scrollViewContentHeight, setScrollViewContentHeight] = useState(0);
|
const [scrollViewContentHeight, setScrollViewContentHeight] = useState(0);
|
||||||
@@ -167,17 +171,42 @@ const ProveScreen: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enhanceApp = async () => {
|
const sessionId = selectedApp.sessionId;
|
||||||
const address = await getPointsAddress();
|
|
||||||
|
|
||||||
// Only update if still the same session
|
if (processedSessionsRef.current.has(sessionId)) {
|
||||||
if (selectedAppRef.current?.sessionId === selectedApp.sessionId) {
|
return;
|
||||||
console.log('enhancing app with points address', address);
|
}
|
||||||
|
|
||||||
|
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({
|
selfClient.getSelfAppState().setSelfApp({
|
||||||
...selectedApp,
|
...currentApp,
|
||||||
selfDefinedData: address.toLowerCase(),
|
selfDefinedData: address.toLowerCase(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processedSessionsRef.current.add(currentSessionId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed enhancing app:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
enhanceApp();
|
enhanceApp();
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ import {
|
|||||||
import { interceptConsole } from '@/services/logging/logger/consoleInterceptor';
|
import { interceptConsole } from '@/services/logging/logger/consoleInterceptor';
|
||||||
import { lokiTransport } from '@/services/logging/logger/lokiTransport';
|
import { lokiTransport } from '@/services/logging/logger/lokiTransport';
|
||||||
import { setupNativeLoggerBridge } from '@/services/logging/logger/nativeLoggerBridge';
|
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<
|
const defaultConfig: configLoggerType<
|
||||||
transportFunctionType<object> | transportFunctionType<object>[],
|
transportFunctionType<object> | transportFunctionType<object>[],
|
||||||
defLvlType
|
defLvlType
|
||||||
> = {
|
> = {
|
||||||
enabled: __DEV__ ? false : true,
|
enabled: __DEV__ ? false : true,
|
||||||
severity: __DEV__ ? 'debug' : 'warn', //TODO configure this using remote-config
|
severity: initialSeverity,
|
||||||
transport: [lokiTransport as unknown as transportFunctionType<object>],
|
transport: [lokiTransport as unknown as transportFunctionType<object>],
|
||||||
transportOptions: {
|
transportOptions: {
|
||||||
colors: {
|
colors: {
|
||||||
@@ -52,6 +56,37 @@ const DocumentLogger = Logger.extend('DOCUMENT');
|
|||||||
//Native Modules
|
//Native Modules
|
||||||
const NfcLogger = Logger.extend('NFC');
|
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
|
// Initialize console interceptor to route console logs to Loki
|
||||||
interceptConsole(AppLogger);
|
interceptConsole(AppLogger);
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,8 @@ export const getWhiteListedDisclosureAddresses = async (): Promise<
|
|||||||
export const hasUserAnIdentityDocumentRegistered =
|
export const hasUserAnIdentityDocumentRegistered =
|
||||||
async (): Promise<boolean> => {
|
async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const { loadDocumentCatalogDirectlyFromKeychain } = await import(
|
const { loadDocumentCatalogDirectlyFromKeychain } =
|
||||||
'@/providers/passportDataProvider'
|
await import('@/providers/passportDataProvider');
|
||||||
);
|
|
||||||
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
|
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
|
||||||
|
|
||||||
return catalog.documents.some(doc => doc.isRegistered === true);
|
return catalog.documents.some(doc => doc.isRegistered === true);
|
||||||
|
|||||||
@@ -129,12 +129,10 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
|
|||||||
|
|
||||||
loadDisclosureEvents: async () => {
|
loadDisclosureEvents: async () => {
|
||||||
try {
|
try {
|
||||||
const { getDisclosurePointEvents } = await import(
|
const { getDisclosurePointEvents } =
|
||||||
'@/services/points/getEvents'
|
await import('@/services/points/getEvents');
|
||||||
);
|
const { useProofHistoryStore } =
|
||||||
const { useProofHistoryStore } = await import(
|
await import('@/stores/proofHistoryStore');
|
||||||
'@/stores/proofHistoryStore'
|
|
||||||
);
|
|
||||||
await useProofHistoryStore.getState().initDatabase();
|
await useProofHistoryStore.getState().initDatabase();
|
||||||
const disclosureEvents = await getDisclosurePointEvents();
|
const disclosureEvents = await getDisclosurePointEvents();
|
||||||
const existingEvents = get().events.filter(e => e.type !== 'disclosure');
|
const existingEvents = get().events.filter(e => e.type !== 'disclosure');
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { create } from 'zustand';
|
|||||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
type LoggingSeverity = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
interface PersistedSettingsState {
|
interface PersistedSettingsState {
|
||||||
addSubscribedTopic: (topic: string) => void;
|
addSubscribedTopic: (topic: string) => void;
|
||||||
biometricsAvailable: boolean;
|
biometricsAvailable: boolean;
|
||||||
@@ -19,6 +21,7 @@ interface PersistedSettingsState {
|
|||||||
homeScreenViewCount: number;
|
homeScreenViewCount: number;
|
||||||
incrementHomeScreenViewCount: () => void;
|
incrementHomeScreenViewCount: () => void;
|
||||||
isDevMode: boolean;
|
isDevMode: boolean;
|
||||||
|
loggingSeverity: LoggingSeverity;
|
||||||
pointsAddress: string | null;
|
pointsAddress: string | null;
|
||||||
removeSubscribedTopic: (topic: string) => void;
|
removeSubscribedTopic: (topic: string) => void;
|
||||||
resetBackupForPoints: () => void;
|
resetBackupForPoints: () => void;
|
||||||
@@ -29,6 +32,7 @@ interface PersistedSettingsState {
|
|||||||
setFcmToken: (token: string | null) => void;
|
setFcmToken: (token: string | null) => void;
|
||||||
setHasViewedRecoveryPhrase: (viewed: boolean) => void;
|
setHasViewedRecoveryPhrase: (viewed: boolean) => void;
|
||||||
setKeychainMigrationCompleted: () => void;
|
setKeychainMigrationCompleted: () => void;
|
||||||
|
setLoggingSeverity: (severity: LoggingSeverity) => void;
|
||||||
setPointsAddress: (address: string | null) => void;
|
setPointsAddress: (address: string | null) => void;
|
||||||
setSubscribedTopics: (topics: string[]) => void;
|
setSubscribedTopics: (topics: string[]) => void;
|
||||||
setTurnkeyBackupEnabled: (turnkeyBackupEnabled: boolean) => void;
|
setTurnkeyBackupEnabled: (turnkeyBackupEnabled: boolean) => void;
|
||||||
@@ -97,6 +101,10 @@ export const useSettingStore = create<SettingsState>()(
|
|||||||
setDevModeOn: () => set({ isDevMode: true }),
|
setDevModeOn: () => set({ isDevMode: true }),
|
||||||
setDevModeOff: () => set({ isDevMode: false }),
|
setDevModeOff: () => set({ isDevMode: false }),
|
||||||
|
|
||||||
|
loggingSeverity: __DEV__ ? 'debug' : 'warn',
|
||||||
|
setLoggingSeverity: (severity: LoggingSeverity) =>
|
||||||
|
set({ loggingSeverity: severity }),
|
||||||
|
|
||||||
hasCompletedKeychainMigration: false,
|
hasCompletedKeychainMigration: false,
|
||||||
setKeychainMigrationCompleted: () =>
|
setKeychainMigrationCompleted: () =>
|
||||||
set({ hasCompletedKeychainMigration: true }),
|
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": {
|
"ios": {
|
||||||
"build": 194,
|
"build": 197,
|
||||||
"lastDeployed": "2025-12-14T22:52:48.122Z"
|
"lastDeployed": "2025-12-25T18:27:37.342Z"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"build": 126,
|
"build": 127,
|
||||||
"lastDeployed": "2025-12-14T22:52:48.122Z"
|
"lastDeployed": "2025-12-17T16:13:30.256Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,10 +86,10 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"ts-mocha": "^10.0.0",
|
"ts-mocha": "^10.0.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22 <23"
|
"node": ">=22 <23"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,7 +708,7 @@
|
|||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22 <23"
|
"node": ">=22 <23"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
"typechain": "^8.3.2",
|
"typechain": "^8.3.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22 <23"
|
"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
|
# 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
|
## React Native Architecture
|
||||||
|
|
||||||
### Navigation System
|
### Navigation System
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
"build:demo": "yarn workspace mobile-sdk-demo build",
|
"build:demo": "yarn workspace mobile-sdk-demo build",
|
||||||
"build:mobile-sdk": "yarn workspace @selfxyz/mobile-sdk-alpha build",
|
"build:mobile-sdk": "yarn workspace @selfxyz/mobile-sdk-alpha build",
|
||||||
"check:versions": "node scripts/check-package-versions.mjs",
|
"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",
|
"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": "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",
|
"format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false",
|
||||||
@@ -65,9 +68,10 @@
|
|||||||
"knip": "^5.63.1",
|
"knip": "^5.63.1",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22 <23"
|
"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-svg": "*",
|
||||||
"react-native-webview": "^13.16.0"
|
"react-native-webview": "^13.16.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "restricted"
|
"access": "restricted"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ if [ -d "$MOBILE_SDK_NATIVE" ]; then
|
|||||||
rm -f "dist/android/mobile-sdk-alpha-release.aar"
|
rm -f "dist/android/mobile-sdk-alpha-release.aar"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update submodule to latest
|
# Setup and update submodule using the setup script
|
||||||
echo "🔄 Updating submodule to latest..."
|
echo "🔄 Setting up and updating submodule..."
|
||||||
git submodule update --init --recursive mobile-sdk-native
|
node scripts/setup-native-source.cjs
|
||||||
|
|
||||||
# Navigate to android directory
|
# Navigate to android directory
|
||||||
cd android
|
cd android
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const path = require('path');
|
|||||||
// Constants
|
// Constants
|
||||||
const SCRIPT_DIR = __dirname;
|
const SCRIPT_DIR = __dirname;
|
||||||
const SDK_DIR = path.dirname(SCRIPT_DIR);
|
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 PRIVATE_MODULE_PATH = path.join(SDK_DIR, 'mobile-sdk-native');
|
||||||
|
|
||||||
const GITHUB_ORG = 'selfxyz';
|
const GITHUB_ORG = 'selfxyz';
|
||||||
@@ -34,10 +35,10 @@ function log(message, type = 'info') {
|
|||||||
console.log(`${prefix} ${message}`);
|
console.log(`${prefix} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runCommand(command, options = {}) {
|
function runCommand(command, options = {}, cwd = SDK_DIR) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
stdio: isDryRun ? 'pipe' : 'inherit',
|
stdio: isDryRun ? 'pipe' : 'inherit',
|
||||||
cwd: SDK_DIR,
|
cwd: cwd,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
@@ -112,18 +113,98 @@ function setupSubmodule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if submodule already exists
|
// Check if submodule is registered in .gitmodules (at repo root)
|
||||||
if (fs.existsSync(PRIVATE_MODULE_PATH)) {
|
const gitmodulesPath = path.join(REPO_ROOT, '.gitmodules');
|
||||||
log('Submodule already exists, updating...', 'info');
|
const gitmodulesExists = fs.existsSync(gitmodulesPath);
|
||||||
runCommand(`git submodule update --init --recursive mobile-sdk-native`);
|
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 {
|
} else {
|
||||||
// Add submodule
|
log('No changes made to .gitmodules - regex may not match', 'warning');
|
||||||
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`;
|
}
|
||||||
|
} 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 {
|
||||||
|
// 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)) {
|
if (isCI && (appToken || repoToken)) {
|
||||||
// Security: Run command silently to avoid token exposure in logs
|
// Security: Run command silently to avoid token exposure in logs
|
||||||
runCommand(addCommand, { stdio: 'pipe' });
|
runCommand(addCommand, { stdio: 'pipe' }, REPO_ROOT);
|
||||||
} else {
|
} else {
|
||||||
runCommand(addCommand);
|
runCommand(addCommand, {}, REPO_ROOT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ interface PressableViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewProps
|
export interface ViewProps
|
||||||
extends Omit<RNViewProps, 'hitSlop'>,
|
extends Omit<RNViewProps, 'hitSlop'>, SpacingProps, Omit<ViewStyle, keyof SpacingProps>, PressableViewProps {}
|
||||||
SpacingProps,
|
|
||||||
Omit<ViewStyle, keyof SpacingProps>,
|
|
||||||
PressableViewProps {}
|
|
||||||
|
|
||||||
const sizeTokens: Record<string, number> = {
|
const sizeTokens: Record<string, number> = {
|
||||||
$0: 0,
|
$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