mirror of
https://github.com/selfxyz/self.git
synced 2026-01-07 22:04:03 -05:00
Add docstring reporting script and workflows (#1333)
* Trim docstring coverage snapshots * format all the tings * update lock * Update docstring coverage snapshots (#1521) * docstring fixes * address agent feedback * update lock files * address agent feedback * lock react-native-svg version to prevent pipeline failures * update docstring logic * remove docstring coverage from ci * remove old report, fix cursorignroe rule
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
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.20.3",
|
||||||
"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.
|
||||||
@@ -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