@hughescr/stryker-bun-runner 1.0.0-beta1

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.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Coverage preload script
3
+ * This script is loaded before tests run to collect mutation coverage data
4
+ * Note: This is a template file with a placeholder import that gets replaced at runtime
5
+ */
6
+
7
+ import { beforeEach, afterEach, afterAll } from 'bun:test';
8
+ import {
9
+ getPreloadConfig,
10
+ shouldCollectCoverage as shouldCollect,
11
+ initializeStrykerNamespace,
12
+ setActiveMutant,
13
+ formatCoverageData,
14
+ writeCoverageToFile,
15
+ parseWebSocketMessage,
16
+ createTestCounter,
17
+ type StrykerNamespace
18
+ } from '__PRELOAD_LOGIC_PATH__';
19
+
20
+ interface StrykerGlobal {
21
+ [key: string]: unknown
22
+ __stryker__?: StrykerNamespace
23
+ __mutantCoverage__?: {
24
+ 'static': Record<string, number>
25
+ perTest: Record<string, Record<string, number>>
26
+ }
27
+ }
28
+
29
+ // Get environment variables
30
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
31
+ const config = getPreloadConfig();
32
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Config from placeholder import
33
+ const { syncPort, coverageFile, activeMutant } = config;
34
+
35
+ // Skip coverage collection during mutant runs (only need pass/fail)
36
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
37
+ const shouldCollectCoverage = shouldCollect(config);
38
+
39
+ // ============================================================================
40
+ // Section 1: WebSocket Sync (receive test start events)
41
+ // ============================================================================
42
+ let ws: WebSocket | null = null;
43
+
44
+ // Track test counter and WebSocket-provided names (only needed when collecting coverage)
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
46
+ const testCounter = createTestCounter();
47
+ let pendingTestName: string | undefined;
48
+
49
+ if(shouldCollectCoverage) {
50
+ // Coverage collection will use these variables
51
+ }
52
+
53
+ if(syncPort && shouldCollectCoverage) {
54
+ try {
55
+ ws = new WebSocket(`ws://localhost:${syncPort}/sync`);
56
+
57
+ ws.onmessage = (event) => {
58
+ const data = String(event.data);
59
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
60
+ const parsedMessage = parseWebSocketMessage(data);
61
+
62
+ if(parsedMessage === 'ready') {
63
+ // Initial ready signal - tests can start
64
+ return;
65
+ }
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Parsed message has dynamic type
68
+ if(parsedMessage && typeof parsedMessage === 'object' && parsedMessage.type === 'testStart') {
69
+ // Store the pending test name to be picked up by beforeEach
70
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- Parsed message has dynamic type
71
+ pendingTestName = parsedMessage.name;
72
+ }
73
+ };
74
+
75
+ // Wait for ready signal
76
+ await new Promise<void>((resolve) => {
77
+ const timeout = setTimeout(() => {
78
+ console.warn('[Stryker] Timeout waiting for ready signal');
79
+ resolve();
80
+ }, 5000);
81
+
82
+ const wsInstance = ws;
83
+ if(!wsInstance) {
84
+ clearTimeout(timeout);
85
+ resolve();
86
+ return;
87
+ }
88
+
89
+ const originalOnMessage = wsInstance.onmessage;
90
+ wsInstance.onmessage = (event) => {
91
+ if(event.data === 'ready') {
92
+ clearTimeout(timeout);
93
+ resolve();
94
+ }
95
+ if(originalOnMessage) {
96
+ originalOnMessage.call(wsInstance, event);
97
+ }
98
+ };
99
+
100
+ wsInstance.onerror = () => {
101
+ clearTimeout(timeout);
102
+ console.warn('[Stryker] Failed to connect to sync server');
103
+ resolve();
104
+ };
105
+ });
106
+ } catch (error) {
107
+ console.warn('[Stryker] Error during synchronization:', error);
108
+ }
109
+ } else if(syncPort && !shouldCollectCoverage) {
110
+ // No coverage collection, just wait for ready signal
111
+ try {
112
+ const ws = new WebSocket(`ws://localhost:${syncPort}/sync`);
113
+ await new Promise<void>((resolve) => {
114
+ const timeout = setTimeout(() => {
115
+ ws.close();
116
+ console.warn('[Stryker Sync] Timeout waiting for ready signal, proceeding anyway');
117
+ resolve();
118
+ }, 5000);
119
+
120
+ ws.onmessage = (event) => {
121
+ if(event.data === 'ready') {
122
+ clearTimeout(timeout);
123
+ ws.close();
124
+ resolve();
125
+ }
126
+ };
127
+
128
+ ws.onerror = () => {
129
+ clearTimeout(timeout);
130
+ console.warn('[Stryker Sync] Failed to connect to sync server, proceeding anyway');
131
+ resolve();
132
+ };
133
+ });
134
+ } catch (error) {
135
+ console.warn('[Stryker Sync] Error during synchronization, proceeding anyway:', error);
136
+ }
137
+ }
138
+
139
+ // ============================================================================
140
+ // Section 2: Initialize Stryker Namespace
141
+ // ============================================================================
142
+ const g = globalThis as unknown as StrykerGlobal;
143
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
144
+ const strykerGlobal = initializeStrykerNamespace(g as Record<string, unknown>);
145
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
146
+ const mutantCoverage = strykerGlobal.mutantCoverage!;
147
+
148
+ // Set active mutant for mutant runs
149
+ if(activeMutant) {
150
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
151
+ setActiveMutant(strykerGlobal, activeMutant);
152
+ }
153
+
154
+ // ============================================================================
155
+ // Section 3: Coverage Writing Logic
156
+ // ============================================================================
157
+
158
+ // Shared coverage writing logic
159
+ const writeCoverageData = () => {
160
+ if(!shouldCollectCoverage || !coverageFile) {
161
+ return;
162
+ }
163
+
164
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- Placeholder import replaced at runtime
165
+ const data = formatCoverageData(strykerGlobal.mutantCoverage, testCounter.getCounterToNameMap());
166
+
167
+ try {
168
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- Placeholder import replaced at runtime
169
+ writeCoverageToFile(coverageFile, data);
170
+ } catch (error) {
171
+ console.error('[Stryker Coverage] Failed to write coverage:', error);
172
+ }
173
+ };
174
+
175
+ // ============================================================================
176
+ // Section 4: Test Hooks (for per-test coverage tracking)
177
+ // ============================================================================
178
+ if(shouldCollectCoverage) {
179
+ beforeEach(() => {
180
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- TestCounter from placeholder import
181
+ const counterId = testCounter.increment();
182
+
183
+ // If we have a pending test name from WebSocket, use it
184
+ if(pendingTestName) {
185
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- TestCounter from placeholder import
186
+ testCounter.setName(counterId, pendingTestName);
187
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
188
+ strykerGlobal.currentTestId = pendingTestName;
189
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
190
+ mutantCoverage.perTest[pendingTestName] ??= {};
191
+ pendingTestName = undefined;
192
+ } else {
193
+ // Fallback to counter - will be remapped later
194
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
195
+ strykerGlobal.currentTestId = counterId;
196
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- MutantCoverage from placeholder import
197
+ mutantCoverage.perTest[counterId] ??= {};
198
+ }
199
+ });
200
+
201
+ afterEach(() => {
202
+ // Clear currentTestId so any subsequent code records to static
203
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- StrykerGlobal from placeholder import
204
+ strykerGlobal.currentTestId = undefined;
205
+ });
206
+
207
+ afterAll(() => {
208
+ ws?.close();
209
+ writeCoverageData();
210
+ });
211
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@hughescr/stryker-bun-runner",
3
+ "version": "1.0.0-beta1",
4
+ "description": "Stryker test runner plugin for Bun with perTest coverage support",
5
+ "keywords": [
6
+ "stryker",
7
+ "stryker-plugin",
8
+ "bun",
9
+ "test-runner",
10
+ "mutation-testing"
11
+ ],
12
+ "homepage": "https://github.com/hughescr/stryker-bun-runner#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/hughescr/stryker-bun-runner/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/hughescr/stryker-bun-runner.git"
19
+ },
20
+ "license": "Apache-2.0",
21
+ "author": "hughescr",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/index.js",
26
+ "types": "./dist/index.d.ts"
27
+ }
28
+ },
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "scripts": {
35
+ "build": "bun build src/index.ts --outdir dist --target node --format esm && bun build src/coverage/preload-logic.ts --outdir dist/coverage --target bun --format esm && dts-bundle-generator --config dts-bundle-generator.config.json && cp -r src/templates dist/",
36
+ "lint": "eslint --cache --format unix .",
37
+ "mutate": "stryker run",
38
+ "prepublishOnly": "bun run build",
39
+ "test": "bun test",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@stryker-mutator/api": "9.4.0",
44
+ "ws": "8.19.0"
45
+ },
46
+ "devDependencies": {
47
+ "@hughescr/eslint-config-default": "4.0.1",
48
+ "@stryker-mutator/core": "9.4.0",
49
+ "@stryker-mutator/typescript-checker": "9.4.0",
50
+ "@types/bun": "1.3.6",
51
+ "@types/node": "25.0.9",
52
+ "@types/ws": "8.18.1",
53
+ "dts-bundle-generator": "9.5.1",
54
+ "eslint": "9.39.2",
55
+ "eslint-formatter-overview": "2.0.0",
56
+ "eslint-formatter-unix": "9.0.1",
57
+ "eslint-plugin-package-json": "0.88.1",
58
+ "typescript": "5.9.3"
59
+ },
60
+ "peerDependencies": {
61
+ "@stryker-mutator/core": "^9.0.0"
62
+ },
63
+ "engines": {
64
+ "bun": ">1.3.6"
65
+ }
66
+ }