react-native-coverage 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Coverage.podspec +20 -0
- package/LICENSE +202 -0
- package/README.md +125 -0
- package/android/build.gradle +51 -0
- package/android/rn-coverage-jacoco.gradle +132 -0
- package/android/rn-coverage.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/coverage/CoverageModule.kt +53 -0
- package/android/src/main/java/com/coverage/CoveragePackage.kt +35 -0
- package/app.plugin.js +1 -0
- package/bin/rn-coverage.js +11 -0
- package/cocoapods/coverage_post_install.rb +197 -0
- package/docs/cli.md +51 -0
- package/docs/config.md +27 -0
- package/docs/index.md +16 -0
- package/docs/integration/android.md +83 -0
- package/docs/integration/ci-appium.md +41 -0
- package/docs/integration/e2e-timing.md +20 -0
- package/docs/integration/ios.md +77 -0
- package/docs/integration/js.md +38 -0
- package/docs/pattern-c.md +5 -0
- package/docs/releasing.md +88 -0
- package/docs.json +24 -0
- package/ios/Coverage.h +5 -0
- package/ios/Coverage.mm +47 -0
- package/ios/CoverageConfig.h +21 -0
- package/ios/CoverageProfile.h +22 -0
- package/ios/CoverageProfile.mm +358 -0
- package/lib/module/NativeCoverage.js +10 -0
- package/lib/module/NativeCoverage.js.map +1 -0
- package/lib/module/assert-coverage.js +181 -0
- package/lib/module/assert-coverage.js.map +1 -0
- package/lib/module/cli/index.js +229 -0
- package/lib/module/cli/index.js.map +1 -0
- package/lib/module/config.js +83 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/exit-codes.js +22 -0
- package/lib/module/exit-codes.js.map +1 -0
- package/lib/module/index.js +35 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/js-coverage.js +172 -0
- package/lib/module/js-coverage.js.map +1 -0
- package/lib/module/node.js +15 -0
- package/lib/module/node.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/path-rewrite.js +44 -0
- package/lib/module/path-rewrite.js.map +1 -0
- package/lib/module/process-ios-native-coverage.js +258 -0
- package/lib/module/process-ios-native-coverage.js.map +1 -0
- package/lib/module/pull-native-coverage.js +210 -0
- package/lib/module/pull-native-coverage.js.map +1 -0
- package/lib/typescript/example-dynamic/App.d.ts +6 -0
- package/lib/typescript/example-dynamic/App.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeCoverage.d.ts +16 -0
- package/lib/typescript/src/NativeCoverage.d.ts.map +1 -0
- package/lib/typescript/src/assert-coverage.d.ts +53 -0
- package/lib/typescript/src/assert-coverage.d.ts.map +1 -0
- package/lib/typescript/src/cli/index.d.ts +4 -0
- package/lib/typescript/src/cli/index.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +93 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/exit-codes.d.ts +16 -0
- package/lib/typescript/src/exit-codes.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/js-coverage.d.ts +36 -0
- package/lib/typescript/src/js-coverage.d.ts.map +1 -0
- package/lib/typescript/src/node.d.ts +14 -0
- package/lib/typescript/src/node.d.ts.map +1 -0
- package/lib/typescript/src/path-rewrite.d.ts +12 -0
- package/lib/typescript/src/path-rewrite.d.ts.map +1 -0
- package/lib/typescript/src/process-ios-native-coverage.d.ts +61 -0
- package/lib/typescript/src/process-ios-native-coverage.d.ts.map +1 -0
- package/lib/typescript/src/pull-native-coverage.d.ts +38 -0
- package/lib/typescript/src/pull-native-coverage.d.ts.map +1 -0
- package/package.json +184 -0
- package/plugin/build/index.d.ts +21 -0
- package/plugin/build/index.js +153 -0
- package/react-native-coverage.config.js.example +47 -0
- package/react-native.config.js +9 -0
- package/src/NativeCoverage.ts +16 -0
- package/src/assert-coverage.ts +260 -0
- package/src/cli/index.ts +420 -0
- package/src/config.ts +189 -0
- package/src/exit-codes.ts +20 -0
- package/src/index.tsx +35 -0
- package/src/js-coverage.ts +213 -0
- package/src/node.ts +52 -0
- package/src/path-rewrite.ts +52 -0
- package/src/process-ios-native-coverage.ts +425 -0
- package/src/pull-native-coverage.ts +269 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Neutral defaults — no consumer product names.
|
|
7
|
+
* Pattern C: this package targets dedicated test apps only.
|
|
8
|
+
*/
|
|
9
|
+
export type SourcePathRewriteRule =
|
|
10
|
+
| {
|
|
11
|
+
kind: 'after-marker';
|
|
12
|
+
/** Substring to locate (e.g. `/packages/`). */
|
|
13
|
+
marker: string;
|
|
14
|
+
/**
|
|
15
|
+
* When true (default), the returned path starts at `marker` without a
|
|
16
|
+
* leading slash (e.g. `packages/...`). When false, returns the substring
|
|
17
|
+
* after the marker.
|
|
18
|
+
*/
|
|
19
|
+
includeMarker?: boolean;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
kind: 'regex';
|
|
23
|
+
pattern: string;
|
|
24
|
+
flags?: string;
|
|
25
|
+
replacement: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type CoverageAssertConfig = {
|
|
29
|
+
/**
|
|
30
|
+
* Substrings that must appear in ≥1 LCOV `SF:` path.
|
|
31
|
+
* Empty → any SF: path counts. Default `packages/` matches common monorepos.
|
|
32
|
+
*/
|
|
33
|
+
lcovPathIncludes: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Case-insensitive substrings matching Jacoco `<package name="">`.
|
|
36
|
+
* Empty falls back to `android.libraryProjectMatchers`, then any package.
|
|
37
|
+
*/
|
|
38
|
+
jacocoPackageIncludes: string[];
|
|
39
|
+
/** Default relative path for `rn-coverage assert --lcov`. */
|
|
40
|
+
defaultLcovPath: string;
|
|
41
|
+
/** Default relative path for `rn-coverage assert --jacoco-xml`. */
|
|
42
|
+
defaultJacocoXmlPath: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type CoverageConfig = {
|
|
46
|
+
/** TurboModule / NativeModules name. Default: `Coverage`. */
|
|
47
|
+
nativeModuleName: string;
|
|
48
|
+
app: {
|
|
49
|
+
androidApplicationId: string;
|
|
50
|
+
iosBundleId: string;
|
|
51
|
+
iosProductName: string;
|
|
52
|
+
};
|
|
53
|
+
ios: {
|
|
54
|
+
/** Framework basename prefixes to include as llvm-cov `-object`s. */
|
|
55
|
+
frameworkNamePrefixes: string[];
|
|
56
|
+
};
|
|
57
|
+
android: {
|
|
58
|
+
libraryProjectMatchers: string[];
|
|
59
|
+
/** Optional staging path used by some e2e runners (e.g. Detox). */
|
|
60
|
+
detoxStagingPath: string;
|
|
61
|
+
coverageRelativePath: string;
|
|
62
|
+
/** Relative path to jacocoTestReport.xml after `android report`. */
|
|
63
|
+
jacocoReportXml: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Istanbul / NYC (TypeScript via source maps). Written by TurboModule
|
|
67
|
+
* `dumpJsCoverage` during `flush()` when Metro is instrumented.
|
|
68
|
+
*/
|
|
69
|
+
js: {
|
|
70
|
+
androidRelativePath: string;
|
|
71
|
+
androidStagingPath: string;
|
|
72
|
+
/** Path under the iOS app data container (usually Documents/…). */
|
|
73
|
+
iosRelativePath: string;
|
|
74
|
+
};
|
|
75
|
+
/** Rules applied left-to-right to LCOV `SF:` paths. */
|
|
76
|
+
sourcePathRewrite: SourcePathRewriteRule[];
|
|
77
|
+
/**
|
|
78
|
+
* When true (recommended in CI), pull/export/report/assert exit 2 if
|
|
79
|
+
* expected hits/artifacts are empty.
|
|
80
|
+
*/
|
|
81
|
+
strict: boolean;
|
|
82
|
+
/** Presence-guard matchers and default artifact paths. */
|
|
83
|
+
assert: CoverageAssertConfig;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const DEFAULT_COVERAGE_CONFIG: CoverageConfig = {
|
|
87
|
+
nativeModuleName: 'Coverage',
|
|
88
|
+
app: {
|
|
89
|
+
androidApplicationId: 'com.example.coverage',
|
|
90
|
+
iosBundleId: 'com.example.coverage',
|
|
91
|
+
iosProductName: 'CoverageExample',
|
|
92
|
+
},
|
|
93
|
+
ios: {
|
|
94
|
+
frameworkNamePrefixes: [],
|
|
95
|
+
},
|
|
96
|
+
android: {
|
|
97
|
+
libraryProjectMatchers: [],
|
|
98
|
+
detoxStagingPath: '/data/local/tmp/coverage/coverage.ec',
|
|
99
|
+
coverageRelativePath: 'files/coverage.ec',
|
|
100
|
+
jacocoReportXml:
|
|
101
|
+
'android/app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml',
|
|
102
|
+
},
|
|
103
|
+
js: {
|
|
104
|
+
androidRelativePath: 'files/coverage-final.json',
|
|
105
|
+
androidStagingPath: '/data/local/tmp/coverage/coverage-final.json',
|
|
106
|
+
iosRelativePath: 'Documents/coverage-final.json',
|
|
107
|
+
},
|
|
108
|
+
sourcePathRewrite: [],
|
|
109
|
+
strict: true,
|
|
110
|
+
assert: {
|
|
111
|
+
lcovPathIncludes: ['packages/'],
|
|
112
|
+
jacocoPackageIncludes: [],
|
|
113
|
+
defaultLcovPath: 'coverage/ios/lcov.info',
|
|
114
|
+
defaultJacocoXmlPath:
|
|
115
|
+
'android/app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type CoverageConfigInput = {
|
|
120
|
+
nativeModuleName?: string;
|
|
121
|
+
app?: Partial<CoverageConfig['app']>;
|
|
122
|
+
ios?: Partial<CoverageConfig['ios']>;
|
|
123
|
+
android?: Partial<CoverageConfig['android']>;
|
|
124
|
+
js?: Partial<CoverageConfig['js']>;
|
|
125
|
+
sourcePathRewrite?: SourcePathRewriteRule[];
|
|
126
|
+
strict?: boolean;
|
|
127
|
+
assert?: Partial<CoverageAssertConfig>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function resolveCoverageConfig(
|
|
131
|
+
input: CoverageConfigInput = {}
|
|
132
|
+
): CoverageConfig {
|
|
133
|
+
return {
|
|
134
|
+
nativeModuleName:
|
|
135
|
+
input.nativeModuleName ?? DEFAULT_COVERAGE_CONFIG.nativeModuleName,
|
|
136
|
+
app: {
|
|
137
|
+
...DEFAULT_COVERAGE_CONFIG.app,
|
|
138
|
+
...input.app,
|
|
139
|
+
},
|
|
140
|
+
ios: {
|
|
141
|
+
...DEFAULT_COVERAGE_CONFIG.ios,
|
|
142
|
+
...input.ios,
|
|
143
|
+
},
|
|
144
|
+
android: {
|
|
145
|
+
...DEFAULT_COVERAGE_CONFIG.android,
|
|
146
|
+
...input.android,
|
|
147
|
+
},
|
|
148
|
+
js: {
|
|
149
|
+
...DEFAULT_COVERAGE_CONFIG.js,
|
|
150
|
+
...input.js,
|
|
151
|
+
},
|
|
152
|
+
sourcePathRewrite:
|
|
153
|
+
input.sourcePathRewrite ?? DEFAULT_COVERAGE_CONFIG.sourcePathRewrite,
|
|
154
|
+
strict: input.strict ?? DEFAULT_COVERAGE_CONFIG.strict,
|
|
155
|
+
assert: {
|
|
156
|
+
...DEFAULT_COVERAGE_CONFIG.assert,
|
|
157
|
+
...input.assert,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const CONFIG_FILENAMES = [
|
|
163
|
+
'react-native-coverage.config.js',
|
|
164
|
+
'react-native-coverage.config.cjs',
|
|
165
|
+
'react-native-coverage.config.mjs',
|
|
166
|
+
] as const;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load config from `cwd` (or `configPath`). Missing file → defaults.
|
|
170
|
+
*/
|
|
171
|
+
export async function loadCoverageConfig(
|
|
172
|
+
cwd: string = process.cwd(),
|
|
173
|
+
configPath?: string
|
|
174
|
+
): Promise<CoverageConfig> {
|
|
175
|
+
const resolvedPath =
|
|
176
|
+
configPath != null
|
|
177
|
+
? path.resolve(cwd, configPath)
|
|
178
|
+
: CONFIG_FILENAMES.map((name) => path.join(cwd, name)).find((p) =>
|
|
179
|
+
fs.existsSync(p)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (resolvedPath == null || !fs.existsSync(resolvedPath)) {
|
|
183
|
+
return resolveCoverageConfig();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const mod = await import(pathToFileURL(resolvedPath).href);
|
|
187
|
+
const raw = (mod.default ?? mod) as CoverageConfigInput;
|
|
188
|
+
return resolveCoverageConfig(raw);
|
|
189
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI / library exit codes for presence / silent-fail guards.
|
|
3
|
+
*
|
|
4
|
+
* Aligns with the RNFB interim assert contract (exit 2 = strict empty).
|
|
5
|
+
*/
|
|
6
|
+
export const EXIT_OK = 0;
|
|
7
|
+
/** Unexpected error / bad invocation / tooling failure. */
|
|
8
|
+
export const EXIT_ERROR = 1;
|
|
9
|
+
/** Strict empty-hit / missing artifact (CI guard). */
|
|
10
|
+
export const EXIT_STRICT_EMPTY = 2;
|
|
11
|
+
|
|
12
|
+
/** Thrown when a strict presence guard fails (map to exit 2 in the CLI). */
|
|
13
|
+
export class StrictEmptyError extends Error {
|
|
14
|
+
readonly exitCode = EXIT_STRICT_EMPTY;
|
|
15
|
+
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'StrictEmptyError';
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Coverage from './NativeCoverage';
|
|
2
|
+
|
|
3
|
+
export { default as Coverage } from './NativeCoverage';
|
|
4
|
+
export type { Spec as CoverageSpec } from './NativeCoverage';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Flush in-process native coverage buffers to disk.
|
|
8
|
+
* iOS: LLVM multi-image (mode c). Android: Jacoco/Emma RT dump.
|
|
9
|
+
*
|
|
10
|
+
* React Native entry only — Node/CLI APIs live under `react-native-coverage/node`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Flush Istanbul JS coverage (when Metro instrumented) then native buffers.
|
|
14
|
+
* No-op for JS dump when `global.__coverage__` is absent.
|
|
15
|
+
*/
|
|
16
|
+
export function flush(): void {
|
|
17
|
+
try {
|
|
18
|
+
const coverage = (globalThis as { __coverage__?: unknown }).__coverage__;
|
|
19
|
+
if (coverage != null && typeof Coverage.dumpJsCoverage === 'function') {
|
|
20
|
+
Coverage.dumpJsCoverage(JSON.stringify(coverage));
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Older native binaries without dumpJsCoverage — native flush still runs.
|
|
24
|
+
}
|
|
25
|
+
Coverage.flush();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Write `global.__coverage__` only (no native Emma/LLVM flush). */
|
|
29
|
+
export function flushJs(): void {
|
|
30
|
+
const coverage = (globalThis as { __coverage__?: unknown }).__coverage__;
|
|
31
|
+
if (coverage == null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
Coverage.dumpJsCoverage(JSON.stringify(coverage));
|
|
35
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import type { CoverageConfig } from './config';
|
|
7
|
+
import { DEFAULT_COVERAGE_CONFIG } from './config';
|
|
8
|
+
import { StrictEmptyError } from './exit-codes';
|
|
9
|
+
import { getAdbBinary, resolveAndroidDeviceId } from './pull-native-coverage';
|
|
10
|
+
|
|
11
|
+
export type JsPullOptions = {
|
|
12
|
+
softFail?: boolean;
|
|
13
|
+
outputDir?: string;
|
|
14
|
+
config?: CoverageConfig;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function ensureDir(dir: string): void {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pull Android Istanbul JSON written by `dumpJsCoverage` (`files/coverage-final.json`).
|
|
23
|
+
*/
|
|
24
|
+
export function pullAndroidJsCoverage(
|
|
25
|
+
deviceId: string,
|
|
26
|
+
options: JsPullOptions = {}
|
|
27
|
+
): string | null {
|
|
28
|
+
const config = options.config ?? DEFAULT_COVERAGE_CONFIG;
|
|
29
|
+
const softFail = options.softFail ?? !config.strict;
|
|
30
|
+
const localDestDir =
|
|
31
|
+
options.outputDir ?? path.resolve(process.cwd(), 'coverage/js');
|
|
32
|
+
const localDestFile = path.join(localDestDir, 'coverage-final.json');
|
|
33
|
+
const relative = config.js.androidRelativePath;
|
|
34
|
+
const staging = config.js.androidStagingPath;
|
|
35
|
+
const adb = getAdbBinary();
|
|
36
|
+
const serial = deviceId ? `-s ${deviceId}` : '';
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const stagingDir = path.posix.dirname(staging);
|
|
40
|
+
execSync(`${adb} ${serial} shell "mkdir -p ${stagingDir}"`);
|
|
41
|
+
execSync(
|
|
42
|
+
`${adb} ${serial} shell "run-as ${config.app.androidApplicationId} cat ${relative} > ${staging}"`
|
|
43
|
+
);
|
|
44
|
+
ensureDir(localDestDir);
|
|
45
|
+
execSync(`${adb} ${serial} pull ${staging} ${localDestFile}`);
|
|
46
|
+
console.log(`JS coverage downloaded to: ${localDestFile}`);
|
|
47
|
+
return localDestFile;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = `Android JS coverage pull failed: ${(error as Error).message}`;
|
|
50
|
+
if (softFail) {
|
|
51
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pull iOS Istanbul JSON from the simulator data container.
|
|
60
|
+
*/
|
|
61
|
+
export function pullIosJsCoverage(
|
|
62
|
+
deviceId: string,
|
|
63
|
+
options: JsPullOptions = {}
|
|
64
|
+
): string | null {
|
|
65
|
+
const config = options.config ?? DEFAULT_COVERAGE_CONFIG;
|
|
66
|
+
const softFail = options.softFail ?? !config.strict;
|
|
67
|
+
const localDestDir =
|
|
68
|
+
options.outputDir ?? path.resolve(process.cwd(), 'coverage/js');
|
|
69
|
+
const localDestFile = path.join(localDestDir, 'coverage-final.json');
|
|
70
|
+
const relative = config.js.iosRelativePath;
|
|
71
|
+
const bundleId = config.app.iosBundleId;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const container = execSync(
|
|
75
|
+
`xcrun simctl get_app_container ${deviceId} ${bundleId} data`,
|
|
76
|
+
{ encoding: 'utf8' }
|
|
77
|
+
).trim();
|
|
78
|
+
const src = path.join(container, relative);
|
|
79
|
+
if (!fs.existsSync(src)) {
|
|
80
|
+
const message = `iOS JS coverage missing at ${src}`;
|
|
81
|
+
if (softFail) {
|
|
82
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
throw new StrictEmptyError(message);
|
|
86
|
+
}
|
|
87
|
+
ensureDir(localDestDir);
|
|
88
|
+
fs.copyFileSync(src, localDestFile);
|
|
89
|
+
console.log(`JS coverage downloaded to: ${localDestFile}`);
|
|
90
|
+
return localDestFile;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof StrictEmptyError) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const message = `iOS JS coverage pull failed: ${(error as Error).message}`;
|
|
96
|
+
if (softFail) {
|
|
97
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type JsReportOptions = {
|
|
105
|
+
/** Istanbul coverage-final.json (or directory of JSON files). */
|
|
106
|
+
input: string;
|
|
107
|
+
/** Directory for lcov.info / HTML (NYC report-dir). */
|
|
108
|
+
outputDir?: string;
|
|
109
|
+
/** Working directory for NYC include globs (usually repo or app root). */
|
|
110
|
+
cwd?: string;
|
|
111
|
+
/** Optional path to nyc.config.js */
|
|
112
|
+
nycConfig?: string;
|
|
113
|
+
softFail?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run NYC report with source-map remap (TypeScript SF: paths).
|
|
118
|
+
* Expects pre-instrumented coverage JSON (`instrument: false` in nyc config).
|
|
119
|
+
*/
|
|
120
|
+
export function reportJsCoverage(options: JsReportOptions): {
|
|
121
|
+
ok: boolean;
|
|
122
|
+
lcovPath: string | null;
|
|
123
|
+
} {
|
|
124
|
+
const cwd = options.cwd ?? process.cwd();
|
|
125
|
+
const outputDir = options.outputDir ?? path.resolve(cwd, 'coverage/js');
|
|
126
|
+
const softFail = options.softFail ?? false;
|
|
127
|
+
const nycOutput = path.join(outputDir, '.nyc_output');
|
|
128
|
+
ensureDir(nycOutput);
|
|
129
|
+
ensureDir(outputDir);
|
|
130
|
+
|
|
131
|
+
const inputPath = path.resolve(options.input);
|
|
132
|
+
if (!fs.existsSync(inputPath)) {
|
|
133
|
+
const message = `JS coverage input missing: ${inputPath}`;
|
|
134
|
+
if (softFail) {
|
|
135
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
136
|
+
return { ok: false, lcovPath: null };
|
|
137
|
+
}
|
|
138
|
+
throw new StrictEmptyError(message);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (fs.statSync(inputPath).isDirectory()) {
|
|
142
|
+
for (const name of fs.readdirSync(inputPath)) {
|
|
143
|
+
if (name.endsWith('.json')) {
|
|
144
|
+
fs.copyFileSync(path.join(inputPath, name), path.join(nycOutput, name));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
fs.copyFileSync(inputPath, path.join(nycOutput, 'coverage-final.json'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const resolveNycBin = (): string => {
|
|
152
|
+
const bases = [cwd, process.cwd()];
|
|
153
|
+
for (const base of bases) {
|
|
154
|
+
try {
|
|
155
|
+
return createRequire(path.join(base, 'package.json')).resolve(
|
|
156
|
+
'nyc/bin/nyc.js'
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
// try next
|
|
160
|
+
}
|
|
161
|
+
const direct = path.join(base, 'node_modules', 'nyc', 'bin', 'nyc.js');
|
|
162
|
+
if (fs.existsSync(direct)) {
|
|
163
|
+
return direct;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw new Error(
|
|
167
|
+
'nyc is required for `rn-coverage js report` — yarn add nyc in the package or test app'
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
const nycBin = resolveNycBin();
|
|
171
|
+
|
|
172
|
+
const args = [
|
|
173
|
+
nycBin,
|
|
174
|
+
'report',
|
|
175
|
+
'--reporter=lcov',
|
|
176
|
+
'--reporter=text-summary',
|
|
177
|
+
`--report-dir=${outputDir}`,
|
|
178
|
+
`--temp-dir=${nycOutput}`,
|
|
179
|
+
'--exclude-after-remap',
|
|
180
|
+
];
|
|
181
|
+
if (options.nycConfig) {
|
|
182
|
+
args.push(`--nycrc-path=${path.resolve(options.nycConfig)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
execFileSync(process.execPath, args, {
|
|
187
|
+
cwd,
|
|
188
|
+
stdio: 'inherit',
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const message = `NYC report failed: ${(error as Error).message}`;
|
|
192
|
+
if (softFail) {
|
|
193
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
194
|
+
return { ok: false, lcovPath: null };
|
|
195
|
+
}
|
|
196
|
+
throw new Error(message);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const lcovPath = path.join(outputDir, 'lcov.info');
|
|
200
|
+
if (!fs.existsSync(lcovPath) || fs.statSync(lcovPath).size === 0) {
|
|
201
|
+
const message = `JS LCOV missing or empty after NYC report: ${lcovPath}`;
|
|
202
|
+
if (softFail) {
|
|
203
|
+
console.warn(`[rn-coverage] ${message}`);
|
|
204
|
+
return { ok: false, lcovPath: null };
|
|
205
|
+
}
|
|
206
|
+
throw new StrictEmptyError(message);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`[rn-coverage] JS LCOV written to ${lcovPath}`);
|
|
210
|
+
return { ok: true, lcovPath };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { resolveAndroidDeviceId };
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node/library surface (CLI helpers, config, assert, pull/export).
|
|
3
|
+
* Do not import from React Native / Metro — use package root `flush()` there.
|
|
4
|
+
*/
|
|
5
|
+
export type {
|
|
6
|
+
CoverageAssertConfig,
|
|
7
|
+
CoverageConfig,
|
|
8
|
+
SourcePathRewriteRule,
|
|
9
|
+
} from './config';
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_COVERAGE_CONFIG,
|
|
12
|
+
loadCoverageConfig,
|
|
13
|
+
resolveCoverageConfig,
|
|
14
|
+
} from './config';
|
|
15
|
+
export { normalizeSourcePath, applySourcePathRewrite } from './path-rewrite';
|
|
16
|
+
export {
|
|
17
|
+
EXIT_OK,
|
|
18
|
+
EXIT_ERROR,
|
|
19
|
+
EXIT_STRICT_EMPTY,
|
|
20
|
+
StrictEmptyError,
|
|
21
|
+
} from './exit-codes';
|
|
22
|
+
export {
|
|
23
|
+
analyzeLcov,
|
|
24
|
+
analyzeJacocoXml,
|
|
25
|
+
assertIosLcov,
|
|
26
|
+
assertAndroidJacoco,
|
|
27
|
+
assertCoverage,
|
|
28
|
+
} from './assert-coverage';
|
|
29
|
+
export type { AssertResult, AssertCoverageOptions } from './assert-coverage';
|
|
30
|
+
export {
|
|
31
|
+
walkFiles,
|
|
32
|
+
collectCoverageObjects,
|
|
33
|
+
resolveIosCoverageContext,
|
|
34
|
+
rewriteLcovFile,
|
|
35
|
+
exportIosLcov,
|
|
36
|
+
reportIosHtml,
|
|
37
|
+
summarizeIos,
|
|
38
|
+
} from './process-ios-native-coverage';
|
|
39
|
+
export {
|
|
40
|
+
resolveAndroidDeviceId,
|
|
41
|
+
androidCoverageFileExists,
|
|
42
|
+
pullAndroidCoverage,
|
|
43
|
+
pullAndroidCoverageWithRetry,
|
|
44
|
+
pullIosCoverage,
|
|
45
|
+
runJacocoTestReport,
|
|
46
|
+
getAdbBinary,
|
|
47
|
+
} from './pull-native-coverage';
|
|
48
|
+
export {
|
|
49
|
+
pullAndroidJsCoverage,
|
|
50
|
+
pullIosJsCoverage,
|
|
51
|
+
reportJsCoverage,
|
|
52
|
+
} from './js-coverage';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SourcePathRewriteRule } from './config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply a single rewrite rule to a source path (forward slashes).
|
|
5
|
+
* Returns the rewritten path, or the input if the rule did not match.
|
|
6
|
+
*/
|
|
7
|
+
export function applySourcePathRewrite(
|
|
8
|
+
sourcePath: string,
|
|
9
|
+
rule: SourcePathRewriteRule
|
|
10
|
+
): string {
|
|
11
|
+
const normalized = sourcePath.replace(/\\/g, '/');
|
|
12
|
+
|
|
13
|
+
if (rule.kind === 'after-marker') {
|
|
14
|
+
const idx = normalized.indexOf(rule.marker);
|
|
15
|
+
if (idx < 0) {
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
const includeMarker = rule.includeMarker !== false;
|
|
19
|
+
if (includeMarker) {
|
|
20
|
+
const fromMarker = normalized.slice(idx);
|
|
21
|
+
return fromMarker.startsWith('/') ? fromMarker.slice(1) : fromMarker;
|
|
22
|
+
}
|
|
23
|
+
return normalized.slice(idx + rule.marker.length);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (rule.kind === 'regex') {
|
|
27
|
+
const re = new RegExp(rule.pattern, rule.flags ?? '');
|
|
28
|
+
if (!re.test(normalized)) {
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
// Reset lastIndex after .test() for global regexes; always rewrite via replace.
|
|
32
|
+
re.lastIndex = 0;
|
|
33
|
+
return normalized.replace(re, rule.replacement);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize an LCOV `SF:` path using ordered rewrite rules.
|
|
41
|
+
* Rules are applied left-to-right; each rule sees the previous result.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeSourcePath(
|
|
44
|
+
sourcePath: string,
|
|
45
|
+
rules: SourcePathRewriteRule[] = []
|
|
46
|
+
): string {
|
|
47
|
+
let current = sourcePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
48
|
+
for (const rule of rules) {
|
|
49
|
+
current = applySourcePathRewrite(current, rule);
|
|
50
|
+
}
|
|
51
|
+
return current;
|
|
52
|
+
}
|