lacuna-cli 0.1.5 → 0.1.7
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/README.md +83 -21
- package/dist/agent/context.d.ts +2 -0
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +314 -1
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/fix-loop.d.ts +5 -0
- package/dist/agent/fix-loop.d.ts.map +1 -1
- package/dist/agent/fix-loop.js +376 -41
- package/dist/agent/fix-loop.js.map +1 -1
- package/dist/agent/generator.d.ts +8 -1
- package/dist/agent/generator.d.ts.map +1 -1
- package/dist/agent/generator.js +89 -11
- package/dist/agent/generator.js.map +1 -1
- package/dist/agent/loop.d.ts +8 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +108 -46
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/{prompts.d.ts → prompts/index.d.ts} +12 -2
- package/dist/agent/prompts/index.d.ts.map +1 -0
- package/dist/agent/prompts/index.js +668 -0
- package/dist/agent/prompts/index.js.map +1 -0
- package/dist/agent/prompts/nextjs.d.ts +12 -0
- package/dist/agent/prompts/nextjs.d.ts.map +1 -0
- package/dist/agent/prompts/nextjs.js +138 -0
- package/dist/agent/prompts/nextjs.js.map +1 -0
- package/dist/agent/prompts/react-native.d.ts +4 -0
- package/dist/agent/prompts/react-native.d.ts.map +1 -0
- package/dist/agent/prompts/react-native.js +82 -0
- package/dist/agent/prompts/react-native.js.map +1 -0
- package/dist/agent/prompts/react.d.ts +2 -0
- package/dist/agent/prompts/react.d.ts.map +1 -0
- package/dist/agent/prompts/react.js +48 -0
- package/dist/agent/prompts/react.js.map +1 -0
- package/dist/agent/prompts/runners/js-common.d.ts +2 -0
- package/dist/agent/prompts/runners/js-common.d.ts.map +1 -0
- package/dist/agent/prompts/runners/js-common.js +13 -0
- package/dist/agent/prompts/runners/js-common.js.map +1 -0
- package/dist/agent/prompts/runners/typescript.d.ts +2 -0
- package/dist/agent/prompts/runners/typescript.d.ts.map +1 -0
- package/dist/agent/prompts/runners/typescript.js +12 -0
- package/dist/agent/prompts/runners/typescript.js.map +1 -0
- package/dist/agent/prompts/runners/vitest.d.ts +2 -0
- package/dist/agent/prompts/runners/vitest.d.ts.map +1 -0
- package/dist/agent/prompts/runners/vitest.js +23 -0
- package/dist/agent/prompts/runners/vitest.js.map +1 -0
- package/dist/agent/prompts/vue.d.ts +3 -0
- package/dist/agent/prompts/vue.d.ts.map +1 -0
- package/dist/agent/prompts/vue.js +29 -0
- package/dist/agent/prompts/vue.js.map +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +43 -32
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/fix.d.ts +2 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +32 -3
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +208 -32
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/config.d.ts +3 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +3 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/coverage/gaps.d.ts +2 -2
- package/dist/lib/coverage/gaps.d.ts.map +1 -1
- package/dist/lib/coverage/gaps.js +35 -5
- package/dist/lib/coverage/gaps.js.map +1 -1
- package/dist/lib/coverage/index.d.ts +1 -1
- package/dist/lib/coverage/index.d.ts.map +1 -1
- package/dist/lib/coverage/index.js +1 -1
- package/dist/lib/coverage/index.js.map +1 -1
- package/dist/lib/detector.d.ts +1 -0
- package/dist/lib/detector.d.ts.map +1 -1
- package/dist/lib/detector.js +41 -8
- package/dist/lib/detector.js.map +1 -1
- package/dist/lib/providers/anthropic.d.ts.map +1 -1
- package/dist/lib/providers/anthropic.js +46 -3
- package/dist/lib/providers/anthropic.js.map +1 -1
- package/dist/lib/providers/openai-compatible.d.ts +1 -1
- package/dist/lib/providers/openai-compatible.d.ts.map +1 -1
- package/dist/lib/providers/openai-compatible.js +43 -2
- package/dist/lib/providers/openai-compatible.js.map +1 -1
- package/dist/lib/providers/types.d.ts +4 -0
- package/dist/lib/providers/types.d.ts.map +1 -1
- package/dist/lib/providers/types.js +10 -0
- package/dist/lib/providers/types.js.map +1 -1
- package/dist/lib/skeleton.d.ts +4 -0
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +220 -0
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/lib/validate.d.ts +1 -0
- package/dist/lib/validate.d.ts.map +1 -1
- package/dist/lib/validate.js +179 -15
- package/dist/lib/validate.js.map +1 -1
- package/dist/lib/worker-display.d.ts +10 -0
- package/dist/lib/worker-display.d.ts.map +1 -1
- package/dist/lib/worker-display.js +53 -5
- package/dist/lib/worker-display.js.map +1 -1
- package/oclif.manifest.json +16 -2
- package/package.json +1 -1
- package/dist/agent/prompts.d.ts.map +0 -1
- package/dist/agent/prompts.js +0 -632
- package/dist/agent/prompts.js.map +0 -1
- package/dist/lib/report-upload.d.ts +0 -3
- package/dist/lib/report-upload.d.ts.map +0 -1
- package/dist/lib/report-upload.js +0 -15
- package/dist/lib/report-upload.js.map +0 -1
package/dist/commands/analyze.js
CHANGED
|
@@ -5,8 +5,9 @@ import { loadConfig } from '../lib/config.js';
|
|
|
5
5
|
import { detectEnvironment } from '../lib/detector.js';
|
|
6
6
|
import { runCommand } from '../lib/runner.js';
|
|
7
7
|
import { startCoverageSpinner } from '../lib/coverage-spinner.js';
|
|
8
|
-
import { loadCoverage, extractGaps, filterTestableGaps, findUncoveredFiles } from '../lib/coverage/index.js';
|
|
8
|
+
import { loadCoverage, extractGaps, filterTestableGaps, findUncoveredFiles, findTestFiles } from '../lib/coverage/index.js';
|
|
9
9
|
import { reportTerminal, buildJsonReport, buildMarkdownReport, getExitCode } from '../lib/reporter.js';
|
|
10
|
+
const EMPTY_REPORT = { files: [], totalLineRate: 0, totalFunctionRate: 0 };
|
|
10
11
|
export default class Analyze extends Command {
|
|
11
12
|
static description = 'Analyze test coverage and show gaps — no files are changed';
|
|
12
13
|
static examples = [
|
|
@@ -52,38 +53,48 @@ export default class Analyze extends Command {
|
|
|
52
53
|
this.log(`${chalk.dim('Detected:')} ${chalk.cyan(env.testRunner)} (${env.language})`);
|
|
53
54
|
this.log(`${chalk.dim('Threshold:')} ${threshold}%\n`);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
// only bail if literally zero tests ran (suites crashed on load)
|
|
65
|
-
const zeroTests = /Tests:\s+0 total|no tests found/i.test(result.stdout + result.stderr);
|
|
66
|
-
if (zeroTests) {
|
|
67
|
-
this.log(chalk.red('\nYour test suites are failing before any tests run.'));
|
|
68
|
-
this.log(chalk.yellow('\nThis usually means:'));
|
|
69
|
-
this.log(' • A missing environment variable (check .env / .env.test)');
|
|
70
|
-
this.log(' • A broken import or missing module');
|
|
71
|
-
this.log(' • A setup file failing (DB connection, mock config, etc.)\n');
|
|
72
|
-
this.log(chalk.dim('Run this to see the actual error:'));
|
|
73
|
-
this.log(chalk.cyan(` ${env.testCommand} 2>&1 | head -80`));
|
|
74
|
-
this.exit(2);
|
|
75
|
-
}
|
|
76
|
-
// partial failures are fine — coverage is still collected for passing tests
|
|
77
|
-
let report;
|
|
78
|
-
try {
|
|
79
|
-
report = await loadCoverage(config);
|
|
56
|
+
// Check if there are any test files before running the coverage command
|
|
57
|
+
const existingTests = await findTestFiles(process.cwd(), {}, config);
|
|
58
|
+
const hasTests = existingTests.length > 0;
|
|
59
|
+
let report = EMPTY_REPORT;
|
|
60
|
+
if (!hasTests) {
|
|
61
|
+
if (flags.format === 'terminal') {
|
|
62
|
+
this.log(chalk.dim(' No test files yet — scanning source files for coverage gaps.\n'));
|
|
63
|
+
}
|
|
80
64
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
else {
|
|
66
|
+
const spinner = startCoverageSpinner(chalk.dim(` Running: ${env.coverageCommand}`), env.testRunner);
|
|
67
|
+
const result = await runCommand(env.coverageCommand, process.cwd(), config.coverageTimeout * 1000, spinner.onLine);
|
|
68
|
+
spinner.stop();
|
|
69
|
+
if (result.timedOut) {
|
|
70
|
+
this.log(chalk.red(`\nTest suite timed out after ${config.coverageTimeout}s.`));
|
|
71
|
+
this.log(chalk.yellow('\nThis usually means a test has an open handle (unclosed server, timer, or connection).'));
|
|
72
|
+
this.log(chalk.dim(`\nIncrease the timeout in .lacuna.json: { "coverageTimeout": ${config.coverageTimeout * 2} }`));
|
|
73
|
+
this.exit(2);
|
|
74
|
+
}
|
|
75
|
+
// bail if suites crashed on load (test files exist but zero tests ran)
|
|
76
|
+
const combined = result.stdout + result.stderr;
|
|
77
|
+
if (/Tests:\s+0 total/i.test(combined)) {
|
|
78
|
+
this.log(chalk.red('\nYour test suites are failing before any tests run.'));
|
|
79
|
+
this.log(chalk.yellow('\nThis usually means:'));
|
|
80
|
+
this.log(' • A missing environment variable (check .env / .env.test)');
|
|
81
|
+
this.log(' • A broken import or missing module');
|
|
82
|
+
this.log(' • A setup file failing (DB connection, mock config, etc.)\n');
|
|
83
|
+
this.log(chalk.dim('Run this to see the actual error:'));
|
|
84
|
+
this.log(chalk.cyan(` ${env.testCommand} 2>&1 | head -80`));
|
|
85
|
+
this.exit(2);
|
|
86
|
+
}
|
|
87
|
+
// partial failures are fine — coverage is still collected for passing tests
|
|
88
|
+
try {
|
|
89
|
+
report = await loadCoverage(config);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
this.log(chalk.red(`Could not read coverage report from ./${config.coverageDir}/\n`));
|
|
93
|
+
this.log(chalk.yellow('Make sure your vitest config has coverage enabled:'));
|
|
94
|
+
this.log(chalk.dim(' // vitest.config.ts'));
|
|
95
|
+
this.log(chalk.dim(' test: { coverage: { reporter: ["lcov", "text-summary"] } }'));
|
|
96
|
+
this.exit(2);
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
99
|
const gaps = await filterTestableGaps(extractGaps(report, threshold), config.ignore);
|
|
89
100
|
// append files that never appeared in the coverage report (never imported by any test)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AACjE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/fix.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export default class Fix extends Command {
|
|
|
9
9
|
model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
workers: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
fresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'regenerate-on-failure': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
'fix-polluters': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
14
|
};
|
|
13
15
|
run(): Promise<void>;
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fix.d.ts","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAA;AAM5C,MAAM,CAAC,OAAO,OAAO,GAAI,SAAQ,OAAO;IACtC,MAAM,CAAC,WAAW,SAAgG;IAElH,MAAM,CAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"fix.d.ts","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAA;AAM5C,MAAM,CAAC,OAAO,OAAO,GAAI,SAAQ,OAAO;IACtC,MAAM,CAAC,WAAW,SAAgG;IAElH,MAAM,CAAC,QAAQ,WAOd;IAED,MAAM,CAAC,KAAK;;;;;;;;;MAoCX;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAqF3B"}
|
package/dist/commands/fix.js
CHANGED
|
@@ -10,6 +10,8 @@ export default class Fix extends Command {
|
|
|
10
10
|
'$ lacuna fix --workers 4',
|
|
11
11
|
'$ lacuna fix --file src/utils/math.test.ts',
|
|
12
12
|
'$ lacuna fix --dry-run',
|
|
13
|
+
'$ lacuna fix --regenerate-on-failure',
|
|
14
|
+
'$ lacuna fix --fix-polluters',
|
|
13
15
|
];
|
|
14
16
|
static flags = {
|
|
15
17
|
'dry-run': Flags.boolean({
|
|
@@ -38,6 +40,15 @@ export default class Fix extends Command {
|
|
|
38
40
|
description: 'Re-run the full test suite even if a recent failing-files cache exists',
|
|
39
41
|
default: false,
|
|
40
42
|
}),
|
|
43
|
+
'regenerate-on-failure': Flags.boolean({
|
|
44
|
+
description: 'If fix exhausts all retries, delete the test and regenerate it from scratch (default: on). Use --no-regenerate-on-failure to disable.',
|
|
45
|
+
default: true,
|
|
46
|
+
allowNo: true,
|
|
47
|
+
}),
|
|
48
|
+
'fix-polluters': Flags.boolean({
|
|
49
|
+
description: 'After fixing, bisect the test suite to identify files that corrupt shared state, then use AI to add cleanup',
|
|
50
|
+
default: false,
|
|
51
|
+
}),
|
|
41
52
|
};
|
|
42
53
|
async run() {
|
|
43
54
|
const { flags } = await this.parse(Fix);
|
|
@@ -69,6 +80,8 @@ export default class Fix extends Command {
|
|
|
69
80
|
targetFile: flags.file,
|
|
70
81
|
workers: flags.workers,
|
|
71
82
|
fresh: flags.fresh,
|
|
83
|
+
regenerateOnFailure: flags['regenerate-on-failure'],
|
|
84
|
+
fixPolluters: flags['fix-polluters'],
|
|
72
85
|
log: (msg) => this.log(msg),
|
|
73
86
|
});
|
|
74
87
|
}
|
|
@@ -79,21 +92,37 @@ export default class Fix extends Command {
|
|
|
79
92
|
this.log(chalk.bold('Results'));
|
|
80
93
|
this.log(` ${chalk.dim('Files processed:')} ${result.filesProcessed}`);
|
|
81
94
|
this.log(` ${chalk.dim('Files fixed:')} ${chalk.green(String(result.filesFixed))}`);
|
|
82
|
-
|
|
95
|
+
if (result.filesAlreadyPassing > 0) {
|
|
96
|
+
this.log(` ${chalk.dim('Already passing:')} ${chalk.dim(String(result.filesAlreadyPassing))}`);
|
|
97
|
+
}
|
|
98
|
+
if (result.pollutersFixed > 0) {
|
|
99
|
+
this.log(` ${chalk.dim('Polluters fixed:')} ${chalk.green(String(result.pollutersFixed))}`);
|
|
100
|
+
}
|
|
101
|
+
if (result.victimsRegenerated > 0) {
|
|
102
|
+
this.log(` ${chalk.dim('Victims regen:')} ${chalk.green(String(result.victimsRegenerated))}`);
|
|
103
|
+
}
|
|
104
|
+
const stillFailing = result.filesProcessed - result.filesFixed - result.filesAlreadyPassing;
|
|
83
105
|
if (stillFailing > 0) {
|
|
84
106
|
this.log(` ${chalk.dim('Still failing:')} ${chalk.red(String(stillFailing))}`);
|
|
85
107
|
}
|
|
108
|
+
if (result.filesAlreadyPassing > 0 && !flags['fix-polluters']) {
|
|
109
|
+
this.log(chalk.dim(`\n ${result.filesAlreadyPassing} file(s) passed in isolation but fail in the suite. Use --fix-polluters to bisect + regenerate them.`));
|
|
110
|
+
}
|
|
86
111
|
if (result.errors.length > 0) {
|
|
87
112
|
this.log(chalk.red(`\n ${result.errors.length} error(s):`));
|
|
88
113
|
for (const err of result.errors) {
|
|
89
|
-
const lines = err.split('\n').slice(0,
|
|
114
|
+
const lines = err.split('\n').slice(0, 15);
|
|
90
115
|
this.log(chalk.dim(' ' + lines.join('\n ')));
|
|
91
116
|
}
|
|
92
117
|
}
|
|
93
118
|
if (result.filesProcessed === 0) {
|
|
94
119
|
this.exit(0);
|
|
95
120
|
}
|
|
96
|
-
else if (
|
|
121
|
+
else if (stillFailing === 0) {
|
|
122
|
+
if (result.filesAlreadyPassing > 0 && result.filesFixed === 0) {
|
|
123
|
+
this.log(chalk.yellow(`\n No tests were repaired — all skipped as already passing. Run lacuna fix --fresh to re-scan.`));
|
|
124
|
+
this.exit(1);
|
|
125
|
+
}
|
|
97
126
|
this.log(chalk.green('\n All failing tests fixed.'));
|
|
98
127
|
this.exit(0);
|
|
99
128
|
}
|
package/dist/commands/fix.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fix.js","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD,MAAM,CAAC,OAAO,OAAO,GAAI,SAAQ,OAAO;IACtC,MAAM,CAAC,WAAW,GAAG,6FAA6F,CAAA;IAElH,MAAM,CAAC,QAAQ,GAAG;QAChB,cAAc;QACd,0BAA0B;QAC1B,4CAA4C;QAC5C,wBAAwB;
|
|
1
|
+
{"version":3,"file":"fix.js","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAwnBrC,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,OAAO;IACvC,MAAM,CAAC,WAAW,SAAiE;IACnF,MAAM,CAAC,QAAQ,WAAoB;IAE7B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA2K3B"}
|
package/dist/commands/init.js
CHANGED
|
@@ -26,9 +26,8 @@ async function readProjectMeta(cwd) {
|
|
|
26
26
|
return { isReact: false, isReactNative: false, isExpo: false, isNextJs: false, isTypeScript: false, isVue: false, isAngular: false, isSvelte: false, isNestJs: false };
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
async function
|
|
30
|
-
// Check
|
|
31
|
-
// package is declared in the root manifest but installed at workspace level)
|
|
29
|
+
async function checkPackageInstallState(pkg, cwd) {
|
|
30
|
+
// Check package.json first — the source of truth for declared dependencies
|
|
32
31
|
try {
|
|
33
32
|
const json = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf-8'));
|
|
34
33
|
const all = {
|
|
@@ -38,17 +37,46 @@ async function isPackageInstalled(pkg, cwd) {
|
|
|
38
37
|
...json['optionalDependencies'],
|
|
39
38
|
};
|
|
40
39
|
if (pkg in all)
|
|
41
|
-
return
|
|
40
|
+
return 'declared';
|
|
42
41
|
}
|
|
43
|
-
catch { /* fall through
|
|
44
|
-
//
|
|
45
|
-
//
|
|
42
|
+
catch { /* fall through */ }
|
|
43
|
+
// Check node_modules — present but undeclared means it was installed on a different
|
|
44
|
+
// branch or manually, and won't survive a fresh CI checkout
|
|
46
45
|
try {
|
|
47
46
|
await access(join(cwd, 'node_modules', pkg));
|
|
48
|
-
return
|
|
47
|
+
return 'undeclared';
|
|
49
48
|
}
|
|
50
49
|
catch { /* not found */ }
|
|
51
|
-
return
|
|
50
|
+
return 'missing';
|
|
51
|
+
}
|
|
52
|
+
// Convenience wrapper used for checking individual extra packages (setupFilePackages)
|
|
53
|
+
async function isPackageInstalled(pkg, cwd) {
|
|
54
|
+
return (await checkPackageInstallState(pkg, cwd)) !== 'missing';
|
|
55
|
+
}
|
|
56
|
+
// Resolves the jest version range that a preset (e.g. jest-expo) declares as its
|
|
57
|
+
// peer dependency, so we install a compatible jest rather than whatever is latest.
|
|
58
|
+
// Falls back to bare 'jest' if the lookup fails (offline, registry unavailable, etc).
|
|
59
|
+
function resolveJestVersionForPreset(preset, cwd) {
|
|
60
|
+
// 1. Check if preset is already installed locally — no network needed
|
|
61
|
+
try {
|
|
62
|
+
const localPkg = JSON.parse(execSync(`cat node_modules/${preset}/package.json 2>/dev/null`, { cwd, encoding: 'utf-8', stdio: 'pipe' }));
|
|
63
|
+
const range = localPkg?.peerDependencies?.jest;
|
|
64
|
+
const major = range?.match(/\d+/)?.[0];
|
|
65
|
+
if (major)
|
|
66
|
+
return { pkg: `jest@${major}`, warned: false };
|
|
67
|
+
}
|
|
68
|
+
catch { /* not installed yet */ }
|
|
69
|
+
// 2. Fall back to npm registry lookup
|
|
70
|
+
try {
|
|
71
|
+
const out = execSync(`npm info ${preset} peerDependencies.jest 2>/dev/null`, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
72
|
+
const cleaned = out.replace(/^['"]|['"]$/g, '');
|
|
73
|
+
const major = cleaned.match(/\d+/)?.[0];
|
|
74
|
+
if (major)
|
|
75
|
+
return { pkg: `jest@${major}`, warned: false };
|
|
76
|
+
}
|
|
77
|
+
catch { /* registry unreachable */ }
|
|
78
|
+
// 3. Could not resolve — warn so the user knows to check manually
|
|
79
|
+
return { pkg: 'jest', warned: true };
|
|
52
80
|
}
|
|
53
81
|
async function writeFileWithDir(filePath, content) {
|
|
54
82
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -91,34 +119,121 @@ async function findProjectRoot(startDir) {
|
|
|
91
119
|
}
|
|
92
120
|
}
|
|
93
121
|
}
|
|
94
|
-
function buildSetupFileContent(variant) {
|
|
122
|
+
function buildSetupFileContent(variant, runner, isExpo = false) {
|
|
123
|
+
// Mock cleanup — prevents spy state from leaking across tests and test files.
|
|
124
|
+
// beforeEach: restores any globalThis spies left by previous files in the same worker
|
|
125
|
+
// (works in concert with restoreMocks: true in vitest.config.ts)
|
|
126
|
+
// afterEach: belt-and-suspenders cleanup within the file
|
|
127
|
+
const vitestCleanup = [
|
|
128
|
+
``,
|
|
129
|
+
`// ── Mock cleanup ──────────────────────────────────────────────────────────`,
|
|
130
|
+
`// restoreMocks/clearMocks in vitest.config.ts handle this automatically,`,
|
|
131
|
+
`// but explicit hooks here guard against any gaps.`,
|
|
132
|
+
`// vi is available globally (globals: true in vitest.config.ts).`,
|
|
133
|
+
``,
|
|
134
|
+
`beforeEach(() => {`,
|
|
135
|
+
` vi.restoreAllMocks()`,
|
|
136
|
+
`})`,
|
|
137
|
+
``,
|
|
138
|
+
`afterEach(() => {`,
|
|
139
|
+
` vi.restoreAllMocks()`,
|
|
140
|
+
` vi.clearAllMocks()`,
|
|
141
|
+
`})`,
|
|
142
|
+
].join('\n');
|
|
143
|
+
const jestCleanup = [
|
|
144
|
+
``,
|
|
145
|
+
`// ── Mock cleanup ──────────────────────────────────────────────────────────`,
|
|
146
|
+
`// Runs after every test to prevent mock state leaking across test files.`,
|
|
147
|
+
`afterEach(() => {`,
|
|
148
|
+
` jest.restoreAllMocks()`,
|
|
149
|
+
` jest.clearAllMocks()`,
|
|
150
|
+
`})`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
const cleanup = runner === 'vitest' ? vitestCleanup : jestCleanup;
|
|
95
153
|
if (variant === 'react-native') {
|
|
154
|
+
const expoMocks = isExpo ? [
|
|
155
|
+
``,
|
|
156
|
+
`// ── Expo module mocks ─────────────────────────────────────────────────────`,
|
|
157
|
+
`jest.mock('expo-constants', () => ({`,
|
|
158
|
+
` default: { expoConfig: { name: 'App', slug: 'app' } },`,
|
|
159
|
+
`}))`,
|
|
160
|
+
``,
|
|
161
|
+
`jest.mock('expo-router', () => ({`,
|
|
162
|
+
` useRouter: jest.fn(() => ({ push: jest.fn(), replace: jest.fn(), back: jest.fn() })),`,
|
|
163
|
+
` useLocalSearchParams: jest.fn(() => ({})),`,
|
|
164
|
+
` usePathname: jest.fn(() => '/'),`,
|
|
165
|
+
` useSegments: jest.fn(() => []),`,
|
|
166
|
+
` Link: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
167
|
+
` router: { push: jest.fn(), replace: jest.fn(), back: jest.fn() },`,
|
|
168
|
+
`}))`,
|
|
169
|
+
``,
|
|
170
|
+
`jest.mock('expo-status-bar', () => ({`,
|
|
171
|
+
` StatusBar: jest.fn(() => null),`,
|
|
172
|
+
`}))`,
|
|
173
|
+
] : [];
|
|
96
174
|
return [
|
|
97
175
|
`// React Native / Expo test setup`,
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
`import React from 'react'`,
|
|
177
|
+
``,
|
|
178
|
+
`// ── Native module mocks ───────────────────────────────────────────────────`,
|
|
179
|
+
`// These modules rely on native code that is unavailable in the Jest environment.`,
|
|
100
180
|
``,
|
|
101
|
-
|
|
181
|
+
`// Safe area context — provides insets/frame for components that use useSafeAreaInsets`,
|
|
182
|
+
`jest.mock('react-native-safe-area-context', () => ({`,
|
|
183
|
+
` SafeAreaProvider: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
184
|
+
` SafeAreaView: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
185
|
+
` useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0, left: 0, right: 0 })),`,
|
|
186
|
+
` useSafeAreaFrame: jest.fn(() => ({ x: 0, y: 0, width: 390, height: 844 })),`,
|
|
187
|
+
`}))`,
|
|
102
188
|
``,
|
|
103
|
-
`//
|
|
104
|
-
`jest.mock('react-native
|
|
189
|
+
`// Gesture handler — required by react-navigation and many UI libraries`,
|
|
190
|
+
`jest.mock('react-native-gesture-handler', () => {`,
|
|
191
|
+
` const RN = jest.requireActual('react-native')`,
|
|
192
|
+
` return {`,
|
|
193
|
+
` ...RN,`,
|
|
194
|
+
` GestureHandlerRootView: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
195
|
+
` PanGestureHandler: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
196
|
+
` TapGestureHandler: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
197
|
+
` Swipeable: jest.fn(({ children }: { children: React.ReactNode }) => children),`,
|
|
198
|
+
` }`,
|
|
199
|
+
`})`,
|
|
200
|
+
``,
|
|
201
|
+
`// AsyncStorage — native async key-value store`,
|
|
202
|
+
`jest.mock('@react-native-async-storage/async-storage', () =>`,
|
|
203
|
+
` jest.requireActual('@react-native-async-storage/async-storage/jest/async-storage-mock')`,
|
|
204
|
+
`)`,
|
|
205
|
+
``,
|
|
206
|
+
`// React Navigation — mock the navigation hooks to avoid needing a real NavigationContainer`,
|
|
207
|
+
`jest.mock('@react-navigation/native', () => ({`,
|
|
208
|
+
` ...jest.requireActual('@react-navigation/native'),`,
|
|
209
|
+
` useNavigation: jest.fn(() => ({ navigate: jest.fn(), goBack: jest.fn(), push: jest.fn(), replace: jest.fn() })),`,
|
|
210
|
+
` useRoute: jest.fn(() => ({ params: {} })),`,
|
|
211
|
+
` useFocusEffect: jest.fn((cb: () => void) => cb()),`,
|
|
212
|
+
` useIsFocused: jest.fn(() => true),`,
|
|
213
|
+
`}))`,
|
|
214
|
+
...expoMocks,
|
|
215
|
+
jestCleanup,
|
|
105
216
|
].join('\n') + '\n';
|
|
106
217
|
}
|
|
107
218
|
if (variant === 'angular') {
|
|
108
|
-
return `import 'jest-preset-angular/setup-jest'\n
|
|
219
|
+
return `import 'jest-preset-angular/setup-jest'\n` + jestCleanup + '\n';
|
|
109
220
|
}
|
|
110
221
|
if (variant === 'nest') {
|
|
111
|
-
return `// NestJS test setup — no DOM environment needed\n
|
|
222
|
+
return `// NestJS test setup — no DOM environment needed\n` + jestCleanup + '\n';
|
|
112
223
|
}
|
|
113
224
|
if (variant === 'vue') {
|
|
114
|
-
return `import '@testing-library/jest-dom'\n
|
|
225
|
+
return `import '@testing-library/jest-dom'\n` + cleanup + '\n';
|
|
115
226
|
}
|
|
116
227
|
if (variant === 'svelte') {
|
|
117
|
-
return `import '@testing-library/jest-dom'\n
|
|
228
|
+
return `import '@testing-library/jest-dom'\n` + cleanup + '\n';
|
|
118
229
|
}
|
|
119
230
|
const lines = [`import '@testing-library/jest-dom'`];
|
|
120
231
|
if (variant === 'nextjs') {
|
|
121
|
-
lines.push(``, `// ── Next.js global mocks ──────────────────────────────────────────────────`, `// These run before every test so individual test files don't need to mock them.`, ``, `import { vi } from 'vitest'`, ``, `// next/navigation — useRouter, usePathname, etc. are server-side and fail in jsdom`, `vi.mock('next/navigation', () => ({`, ` useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), prefetch: vi.fn() })),`, ` usePathname: vi.fn(() => '/'),`, ` useSearchParams: vi.fn(() => new URLSearchParams()),`, ` useParams: vi.fn(() => ({})),`, ` redirect: vi.fn(),`, ` notFound: vi.fn(),`, `}))`, ``, `// next/headers — server-only, throws in jsdom`, `vi.mock('next/headers', () => ({`, ` cookies: vi.fn(() => ({ get: vi.fn(), set: vi.fn(), delete: vi.fn(), has: vi.fn(), getAll: vi.fn(() => []) })),`, ` headers: vi.fn(() => new Headers()),`, `}))`, ``, `// next/cache — no-ops in tests`, `vi.mock('next/cache', () => ({`, ` revalidatePath: vi.fn(),`, ` revalidateTag: vi.fn(),`, ` unstable_cache: vi.fn((fn: () => unknown) => fn),`, `}))`, ``, `// next/image — uses Next.js image optimization which breaks in jsdom`, `vi.mock('next/image', () => ({`, ` default: vi.fn(({ src, alt, ...props }: Record<string, unknown>) => null),`, `}))`, ``, `// next/font — font loading tries to fetch/read files at import time, fails in tests`, `// Add any fonts your project uses that aren't listed here`, `vi.mock('next/font/google', () => new Proxy({}, {`, ` get: (_: object, fontName: string) =>`, ` () => ({ className: \`font-\${fontName.toLowerCase()}\`, style: { fontFamily: fontName } }),`, `}))`, `vi.mock('next/font/local', () => ({`, ` default: vi.fn(() => ({ className: 'font-local', style: { fontFamily: 'local' } })),`, `}))
|
|
232
|
+
lines.push(``, `// ── Next.js global mocks ──────────────────────────────────────────────────`, `// These run before every test so individual test files don't need to mock them.`, ``, `import { vi, beforeEach, afterEach } from 'vitest'`, ``, `// next/navigation — useRouter, usePathname, etc. are server-side and fail in jsdom`, `vi.mock('next/navigation', () => ({`, ` useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), prefetch: vi.fn() })),`, ` usePathname: vi.fn(() => '/'),`, ` useSearchParams: vi.fn(() => new URLSearchParams()),`, ` useParams: vi.fn(() => ({})),`, ` redirect: vi.fn(),`, ` notFound: vi.fn(),`, `}))`, ``, `// next/headers — server-only, throws in jsdom`, `vi.mock('next/headers', () => ({`, ` cookies: vi.fn(() => ({ get: vi.fn(), set: vi.fn(), delete: vi.fn(), has: vi.fn(), getAll: vi.fn(() => []) })),`, ` headers: vi.fn(() => new Headers()),`, `}))`, ``, `// next/cache — no-ops in tests`, `vi.mock('next/cache', () => ({`, ` revalidatePath: vi.fn(),`, ` revalidateTag: vi.fn(),`, ` unstable_cache: vi.fn((fn: () => unknown) => fn),`, `}))`, ``, `// next/image — uses Next.js image optimization which breaks in jsdom`, `vi.mock('next/image', () => ({`, ` default: vi.fn(({ src, alt, ...props }: Record<string, unknown>) => null),`, `}))`, ``, `// next/font — font loading tries to fetch/read files at import time, fails in tests`, `// Add any fonts your project uses that aren't listed here`, `vi.mock('next/font/google', () => new Proxy({}, {`, ` get: (_: object, fontName: string) =>`, ` () => ({ className: \`font-\${fontName.toLowerCase()}\`, style: { fontFamily: fontName } }),`, `}))`, `vi.mock('next/font/local', () => ({`, ` default: vi.fn(() => ({ className: 'font-local', style: { fontFamily: 'local' } })),`, `}))`, vitestCleanup);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// plain react
|
|
236
|
+
lines.push(cleanup);
|
|
122
237
|
}
|
|
123
238
|
return lines.join('\n') + '\n';
|
|
124
239
|
}
|
|
@@ -146,7 +261,7 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
146
261
|
return undefined;
|
|
147
262
|
}
|
|
148
263
|
const meta = await readProjectMeta(cwd);
|
|
149
|
-
const
|
|
264
|
+
const installState = await checkPackageInstallState(runner, cwd);
|
|
150
265
|
// ── Determine packages to install ─────────────────────────────────────────
|
|
151
266
|
const basePackages = [];
|
|
152
267
|
const setupFilePackages = [];
|
|
@@ -173,11 +288,22 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
173
288
|
// @vitejs/plugin-react is NOT needed for Vitest — esbuild handles JSX/TSX natively.
|
|
174
289
|
}
|
|
175
290
|
else if (runner === 'jest') {
|
|
176
|
-
if (meta.
|
|
291
|
+
if (meta.isReactNative) {
|
|
292
|
+
// Use the jest version that the preset actually supports — resolved at init time
|
|
293
|
+
// so this stays correct when jest-expo bumps its peer dep in the future.
|
|
294
|
+
const rnPreset = meta.isExpo ? 'jest-expo' : '@react-native/jest-preset';
|
|
295
|
+
const { pkg: jestPkg, warned } = resolveJestVersionForPreset(rnPreset, cwd);
|
|
296
|
+
if (warned) {
|
|
297
|
+
log(chalk.yellow(` ⚠ Could not resolve compatible jest version for ${rnPreset}. Installing latest — if tests fail with version mismatch errors, pin jest to the version in ${rnPreset}'s peerDependencies.`));
|
|
298
|
+
}
|
|
299
|
+
const jestMajor = jestPkg.includes('@') ? jestPkg.split('@')[1] : '';
|
|
300
|
+
basePackages.push(jestPkg, jestMajor ? `@types/jest@${jestMajor}` : '@types/jest');
|
|
301
|
+
}
|
|
302
|
+
else if (meta.isTypeScript) {
|
|
177
303
|
basePackages.push('jest', '@types/jest', 'ts-jest');
|
|
178
304
|
}
|
|
179
305
|
else {
|
|
180
|
-
basePackages.push('jest');
|
|
306
|
+
basePackages.push('jest', '@types/jest');
|
|
181
307
|
}
|
|
182
308
|
if (meta.isReactNative) {
|
|
183
309
|
// Don't add jest-environment-jsdom for RN
|
|
@@ -227,7 +353,7 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
227
353
|
return undefined;
|
|
228
354
|
})();
|
|
229
355
|
// ── Install missing packages ───────────────────────────────────────────────
|
|
230
|
-
if (
|
|
356
|
+
if (installState === 'missing') {
|
|
231
357
|
const allPackages = [...basePackages, ...setupFilePackages];
|
|
232
358
|
log(chalk.yellow(`\n ${runner} is not installed.`));
|
|
233
359
|
log(chalk.dim(` Packages: ${allPackages.join(', ')}`));
|
|
@@ -245,6 +371,30 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
245
371
|
}
|
|
246
372
|
}
|
|
247
373
|
}
|
|
374
|
+
else if (installState === 'undeclared') {
|
|
375
|
+
// Package exists in node_modules but is NOT declared in package.json.
|
|
376
|
+
// This usually means it was installed on a different branch and won't survive
|
|
377
|
+
// a fresh CI checkout — node_modules is not committed to git.
|
|
378
|
+
const allPackages = [...basePackages, ...setupFilePackages];
|
|
379
|
+
log(chalk.yellow(`\n ${runner} was found in node_modules but is not declared in package.json.`));
|
|
380
|
+
log(chalk.dim(` This works locally but will break CI — a fresh checkout won't have node_modules.`));
|
|
381
|
+
const doAdd = await confirm({
|
|
382
|
+
message: `Add ${allPackages.join(', ')} to package.json? (recommended for CI)`,
|
|
383
|
+
default: true,
|
|
384
|
+
});
|
|
385
|
+
if (!doAdd) {
|
|
386
|
+
log(chalk.dim(` Skipped. Add manually: npm install -D ${allPackages.join(' ')}`));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
log(chalk.dim(`\n Adding to package.json...`));
|
|
390
|
+
try {
|
|
391
|
+
execSync(`npm install -D ${allPackages.join(' ')}`, { cwd, stdio: 'inherit' });
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
log(chalk.red(` Failed. Run manually: npm install -D ${allPackages.join(' ')}`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
248
398
|
else if ((meta.isNextJs || meta.isReact) && setupFilePackages.length > 0) {
|
|
249
399
|
// Runner is installed — check if the extra testing-library packages are present.
|
|
250
400
|
// Must use a for-loop: Array.filter ignores async callbacks (the Promise is always truthy).
|
|
@@ -280,11 +430,23 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
280
430
|
: meta.isVue ? 'vue'
|
|
281
431
|
: meta.isSvelte ? 'svelte'
|
|
282
432
|
: 'react';
|
|
283
|
-
const setupContent = buildSetupFileContent(setupVariant);
|
|
433
|
+
const setupContent = buildSetupFileContent(setupVariant, runner, meta.isExpo);
|
|
284
434
|
await writeFileWithDir(absSetup, setupContent);
|
|
285
435
|
log(chalk.green(` ✓ Created ${setupFilePath}`));
|
|
286
|
-
if (meta.isNextJs)
|
|
436
|
+
if (meta.isNextJs) {
|
|
287
437
|
log(chalk.dim(` Includes global mocks for next/navigation, next/headers, next/cache`));
|
|
438
|
+
// Create the empty module that the server-only alias points to.
|
|
439
|
+
// Without this file, Vitest crashes when any source file imports 'server-only'.
|
|
440
|
+
const emptyModulePath = resolve(cwd, 'test/empty-module.ts');
|
|
441
|
+
try {
|
|
442
|
+
await access(emptyModulePath);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
await writeFileWithDir(emptyModulePath, 'export default {}\n');
|
|
446
|
+
log(chalk.green(` ✓ Created test/empty-module.ts`));
|
|
447
|
+
log(chalk.dim(` Used as the server-only alias target in vitest.config.ts`));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
288
450
|
createdSetupFile = setupFilePath;
|
|
289
451
|
}
|
|
290
452
|
}
|
|
@@ -306,8 +468,14 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
306
468
|
// to stay consistent with whatever the project has configured.
|
|
307
469
|
// No React plugin needed: Vitest uses esbuild which handles JSX/TSX natively.
|
|
308
470
|
const aliasTarget = meta.isNextJs ? await resolveAtAlias(cwd) : null;
|
|
471
|
+
// Next.js: add server-only alias so Vitest doesn't crash on Next.js server-only imports.
|
|
472
|
+
// server-only is a Next.js guard that throws at build time if server code leaks to the client;
|
|
473
|
+
// in Vitest it just needs to resolve to something harmless.
|
|
474
|
+
const serverOnlyAlias = meta.isNextJs
|
|
475
|
+
? `,\n 'server-only': path.resolve(__dirname, './test/empty-module.ts')`
|
|
476
|
+
: '';
|
|
309
477
|
const aliasBlock = aliasTarget
|
|
310
|
-
? `\n resolve: {\n alias: { '@': path.resolve(__dirname, '${aliasTarget}') },\n },`
|
|
478
|
+
? `\n resolve: {\n alias: { '@': path.resolve(__dirname, '${aliasTarget}')${serverOnlyAlias} },\n },`
|
|
311
479
|
: '';
|
|
312
480
|
const pathImport = aliasTarget ? `import path from 'path'\n` : '';
|
|
313
481
|
const vuePlugin = meta.isVue ? `\nimport vue from '@vitejs/plugin-vue'` : '';
|
|
@@ -323,6 +491,12 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
323
491
|
`export default defineConfig({${aliasBlock}${pluginsBlock}`,
|
|
324
492
|
` test: {`,
|
|
325
493
|
` globals: true,${envLine}${setupLine}`,
|
|
494
|
+
` // Restore and clear all mocks automatically before each test.`,
|
|
495
|
+
` // restoreMocks runs at the Vitest worker level and can restore globalThis spies`,
|
|
496
|
+
` // that the module-level vi instance cannot see — preventing cross-file contamination`,
|
|
497
|
+
` // when multiple test files share the same worker thread.`,
|
|
498
|
+
` restoreMocks: true,`,
|
|
499
|
+
` clearMocks: true,`,
|
|
326
500
|
` coverage: {`,
|
|
327
501
|
` provider: 'v8',`,
|
|
328
502
|
` reporter: ['lcov', 'text-summary'],`,
|
|
@@ -345,17 +519,19 @@ async function ensureTestRunnerSetup(runner, sourceDir, cwd, log) {
|
|
|
345
519
|
}
|
|
346
520
|
catch {
|
|
347
521
|
const setupLine = createdSetupFile
|
|
348
|
-
? `\n
|
|
522
|
+
? `\n setupFilesAfterEnv: ['<rootDir>/${createdSetupFile}'],`
|
|
349
523
|
: '';
|
|
350
524
|
const needsJsdom = (meta.isReact || meta.isVue || meta.isSvelte) && !meta.isReactNative && !meta.isAngular && !meta.isNestJs;
|
|
351
525
|
const envLine = needsJsdom ? `\n testEnvironment: 'jsdom',` : '';
|
|
352
|
-
|
|
526
|
+
// React Native / Expo: babel-preset-expo already handles TypeScript — don't override transform
|
|
527
|
+
// or it replaces the preset's JS transform and setup.js files fail to parse.
|
|
528
|
+
const tsLines = meta.isTypeScript && !meta.isReactNative
|
|
353
529
|
? `\n transform: { '^.+\\\\.tsx?$': 'ts-jest' },`
|
|
354
530
|
: '';
|
|
355
531
|
const rnPreset = meta.isExpo ? 'jest-expo' : 'react-native';
|
|
356
532
|
const presetLine = meta.isReactNative ? `\n preset: '${rnPreset}',` : '';
|
|
357
533
|
const transformIgnoreLine = meta.isReactNative
|
|
358
|
-
? `\n transformIgnorePatterns: ['node_modules/(?!(react-native
|
|
534
|
+
? `\n transformIgnorePatterns: ['node_modules/(?!(.pnpm/[^/]*/node_modules/)?(react-native(-[^/]+)?|@react-native|@react-navigation|expo(-[^/]+)?|@expo|@testing-library)/)'],`
|
|
359
535
|
: '';
|
|
360
536
|
const angularPreset = meta.isAngular ? `\n preset: 'jest-preset-angular',` : '';
|
|
361
537
|
const content = [
|
|
@@ -488,7 +664,7 @@ export default class Init extends Command {
|
|
|
488
664
|
if (hasMocks) {
|
|
489
665
|
mocksFile = await input({
|
|
490
666
|
message: 'Path to shared mock file:',
|
|
491
|
-
default: `${sourceDir}/test/mocks.ts`,
|
|
667
|
+
default: (await readProjectMeta(cwd)).isReactNative ? `${sourceDir}/test/mock.tsx` : `${sourceDir}/test/mocks.ts`,
|
|
492
668
|
});
|
|
493
669
|
}
|
|
494
670
|
// ── Coverage threshold ────────────────────────────────────────────────────
|
|
@@ -502,7 +678,7 @@ export default class Init extends Command {
|
|
|
502
678
|
testRunner: testRunner,
|
|
503
679
|
coverageFormat: 'lcov',
|
|
504
680
|
coverageDir: 'coverage',
|
|
505
|
-
sourceDir,
|
|
681
|
+
sourceDir: [sourceDir],
|
|
506
682
|
threshold,
|
|
507
683
|
maxIterations: 3,
|
|
508
684
|
};
|