flake-monster 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/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/flake-monster.js +6 -0
- package/package.json +48 -0
- package/src/adapters/adapter-interface.js +86 -0
- package/src/adapters/javascript/codegen.js +13 -0
- package/src/adapters/javascript/index.js +76 -0
- package/src/adapters/javascript/injector.js +438 -0
- package/src/adapters/javascript/parser.js +19 -0
- package/src/adapters/javascript/remover.js +128 -0
- package/src/adapters/registry.js +64 -0
- package/src/cli/commands/inject.js +64 -0
- package/src/cli/commands/restore.js +107 -0
- package/src/cli/commands/test.js +215 -0
- package/src/cli/index.js +19 -0
- package/src/core/config.js +57 -0
- package/src/core/engine.js +156 -0
- package/src/core/flake-analyzer.js +64 -0
- package/src/core/manifest.js +137 -0
- package/src/core/parsers/index.js +40 -0
- package/src/core/parsers/jest.js +52 -0
- package/src/core/parsers/node-test.js +64 -0
- package/src/core/parsers/tap.js +92 -0
- package/src/core/profile.js +72 -0
- package/src/core/reporter.js +75 -0
- package/src/core/seed.js +59 -0
- package/src/core/workspace.js +139 -0
- package/src/runtime/javascript/flake-monster.runtime.js +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 growthboot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# FlakeMonster
|
|
2
|
+
|
|
3
|
+
A source-to-source test hardener that finds flaky tests by injecting async delays into your code. It intentionally makes timing worse so race conditions surface *before* they hit production, then gives you a seed to reproduce the exact failure every time.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Automated tests run unrealistically fast. API calls resolve instantly against local mocks, database queries return in microseconds, commands are fired fasters than you can blink and eye, and every async operation completes in the exact same order every time. In production, none of that is true. Network latency varies, services respond unpredictably, computer performance varies, devices glitch, and users interact at human speed. Tests that pass in this perfectly-timed environment can hide real bugs, race conditions, missing `await`s, and unguarded state mutations, that only surface when timing shifts even slightly.
|
|
8
|
+
|
|
9
|
+
FlakeMonster closes that gap. It **deliberately injects async delays** between statements in your `async` functions and at the module top level (using top-level `await`), forcing the event loop to yield where it normally wouldn't. Tests that depend on everything happening in a precise order will start failing, and that's the point. A test that only passes because it runs too fast to trigger its own race condition **should not be passing**.
|
|
10
|
+
|
|
11
|
+
The goal isn't to slow your tests down. The goal is to increasing flake likelyhood random timing glitches so that you can catch the test flakes on your first tests.
|
|
12
|
+
|
|
13
|
+
Every run uses a **deterministic seed**, so when a test fails you get output like:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Run 7/10 FAIL (seed=48291)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Re-run with that seed and you'll get the same failure every time. You can inject the delays, leave them there while you fix the issues, then remove them when you're done.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install flake-monster
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node.js >= 18.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Find flaky tests in 30 seconds:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Run your test suite 10 times with injected delays
|
|
35
|
+
npx flake-monster test --cmd "npm test"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it. FlakeMonster will:
|
|
39
|
+
1. Inject `await` delays between statements in async functions and at the module top level, directly in your source files
|
|
40
|
+
2. Run your tests against the modified code
|
|
41
|
+
3. Repeat with a different seed each run
|
|
42
|
+
4. Restore your files to their original state
|
|
43
|
+
5. Report which runs failed and the seeds to reproduce them
|
|
44
|
+
|
|
45
|
+
## Use Cases
|
|
46
|
+
|
|
47
|
+
**Validate AI-generated code**, Coding agents like Claude Code and Codex write tests that pass once and move on. Have the agent run tests through FlakeMonster instead to catch missing `await`s and state races that a normal test run misses because it executes too fast and predictably.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# After an agent writes async code, verify it isn't flaky
|
|
51
|
+
flake-monster test --runs 5 --cmd "npm test"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Browser-based test suites**, Works with Playwright, Cypress, Vitest browser mode, anything that bundles or serves JS. No Node loader hooks, no plugins, no configuration.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
flake-monster test --cmd "npx playwright test" "src/**/*.js"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**CI flake gate**, Block merges that introduce flaky tests.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
flake-monster test --runs 10 --cmd "npm test"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Reproduce and debug**, Every failure comes with a seed. Inject delays, debug freely, delays stay pinned while you edit around them.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
flake-monster inject --seed 48291 "src/**/*.js"
|
|
70
|
+
# add console.logs, set breakpoints, iterate, delays don't move
|
|
71
|
+
flake-monster restore
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
See [Workflows](WORKFLOWS.md) for full details on each.
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
### `flake-monster test`
|
|
79
|
+
|
|
80
|
+
The main command. Runs your tests multiple times with different delay patterns to surface flakes.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Basic, 10 runs, medium density
|
|
84
|
+
flake-monster test --cmd "npm test"
|
|
85
|
+
|
|
86
|
+
# More aggressive, 20 runs, maximum delay injection
|
|
87
|
+
flake-monster test --runs 20 --mode hardcore --cmd "npm test"
|
|
88
|
+
|
|
89
|
+
# Target specific files
|
|
90
|
+
flake-monster test --cmd "npm test" "src/api/**/*.js" "src/services/**/*.js"
|
|
91
|
+
|
|
92
|
+
# Reproduce a specific failure
|
|
93
|
+
flake-monster test --runs 1 --seed 48291 --cmd "npm test"
|
|
94
|
+
|
|
95
|
+
# Use workspace copies instead of modifying source files directly
|
|
96
|
+
flake-monster test --workspace --cmd "npm test" --keep-on-fail
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Option | Default | Description |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `-r, --runs <n>` | `10` | Number of test runs |
|
|
102
|
+
| `-m, --mode <mode>` | `medium` | Injection density: `light`, `medium`, `hardcore` |
|
|
103
|
+
| `-s, --seed <seed>` | `auto` | Base seed (`auto` generates one randomly) |
|
|
104
|
+
| `-c, --cmd <command>` | `npm test` | Test command to execute |
|
|
105
|
+
| `--in-place` | `true` | Modify source files directly (default) |
|
|
106
|
+
| `--workspace` | `false` | Use workspace copies instead of modifying source files |
|
|
107
|
+
| `--keep-on-fail` | `false` | Keep workspace on failure for inspection (workspace mode only) |
|
|
108
|
+
| `--keep-all` | `false` | Keep all workspaces (workspace mode only) |
|
|
109
|
+
| `--min-delay <ms>` | `0` | Minimum delay in milliseconds |
|
|
110
|
+
| `--max-delay <ms>` | `50` | Maximum delay in milliseconds |
|
|
111
|
+
|
|
112
|
+
### `flake-monster inject`
|
|
113
|
+
|
|
114
|
+
Inject delays without running tests. Useful for manual inspection or running tests yourself.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Inject in-place (default)
|
|
118
|
+
flake-monster inject "src/**/*.js"
|
|
119
|
+
|
|
120
|
+
# Inject into a workspace copy instead
|
|
121
|
+
flake-monster inject --workspace "src/**/*.js"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `flake-monster restore`
|
|
125
|
+
|
|
126
|
+
Remove all injected delays and restore original source.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
flake-monster restore
|
|
130
|
+
|
|
131
|
+
# Recovery mode: interactive scan and confirm, use when traces remain after a normal restore
|
|
132
|
+
flake-monster restore --recover
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Modes
|
|
136
|
+
|
|
137
|
+
Modes control how many delays get injected:
|
|
138
|
+
|
|
139
|
+
- **`light`**, One delay at the top of each async function and one at the first module-level statement. Good for a quick sanity check.
|
|
140
|
+
- **`medium`**, Delays between statements, skipping before `return`/`throw`. Applies to both async function bodies and top-level module scope. The default, catches most race conditions without being overwhelming.
|
|
141
|
+
- **`hardcore`**, Delays between nearly every statement, everywhere. Maximum chaos. Use this when medium isn't surfacing a suspected flake.
|
|
142
|
+
|
|
143
|
+
## What Gets Injected
|
|
144
|
+
|
|
145
|
+
FlakeMonster injects delays in two places: **inside async function bodies** and **at the module top level** (using top-level `await`).
|
|
146
|
+
|
|
147
|
+
### Inside async functions
|
|
148
|
+
|
|
149
|
+
Given this code:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
async function loadUser(id) {
|
|
153
|
+
const user = await api.getUser(id);
|
|
154
|
+
const prefs = await api.getPrefs(id);
|
|
155
|
+
return { ...user, ...prefs };
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
FlakeMonster (in `medium` mode) produces something like:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { __FlakeMonster__ } from "./flake-monster.runtime.js";
|
|
163
|
+
|
|
164
|
+
async function loadUser(id) {
|
|
165
|
+
/* @flake-monster[jt92-se2j!] v1 */
|
|
166
|
+
await __FlakeMonster__(23);
|
|
167
|
+
const user = await api.getUser(id);
|
|
168
|
+
/* @flake-monster[jt92-se2j!] v1 */
|
|
169
|
+
await __FlakeMonster__(41);
|
|
170
|
+
const prefs = await api.getPrefs(id);
|
|
171
|
+
return { ...user, ...prefs };
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### At the module top level
|
|
176
|
+
|
|
177
|
+
Top-level `await` is valid in ES modules. FlakeMonster also injects delays between top-level statements to surface races in module initialization order:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { fetchData } from './api.js';
|
|
181
|
+
|
|
182
|
+
const config = await fetchData('/config');
|
|
183
|
+
const user = await fetchData(`/users/${config.defaultId}`);
|
|
184
|
+
|
|
185
|
+
export { config, user };
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Becomes:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
import { fetchData } from './api.js';
|
|
192
|
+
import { __FlakeMonster__ } from "./flake-monster.runtime.js";
|
|
193
|
+
|
|
194
|
+
/* @flake-monster[jt92-se2j!] v1 */
|
|
195
|
+
await __FlakeMonster__(17);
|
|
196
|
+
const config = await fetchData('/config');
|
|
197
|
+
/* @flake-monster[jt92-se2j!] v1 */
|
|
198
|
+
await __FlakeMonster__(38);
|
|
199
|
+
const user = await fetchData(`/users/${config.defaultId}`);
|
|
200
|
+
/* @flake-monster[jt92-se2j!] v1 */
|
|
201
|
+
await __FlakeMonster__(9);
|
|
202
|
+
export { config, user };
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Each `await __FlakeMonster__(N)` call yields back to the event loop for a short, deterministic duration derived from the seed + location. This is enough to reorder microtask scheduling and expose races.
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
Create a `.flakemonsterrc.json` or `flakemonster.config.json` in your project root:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"include": ["src/**/*.js"],
|
|
214
|
+
"exclude": ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
215
|
+
"mode": "medium",
|
|
216
|
+
"minDelayMs": 0,
|
|
217
|
+
"maxDelayMs": 50,
|
|
218
|
+
"testCommand": "npm test",
|
|
219
|
+
"runs": 10,
|
|
220
|
+
"keepOnFail": true,
|
|
221
|
+
"skipTryCatch": false,
|
|
222
|
+
"skipGenerators": true
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
CLI flags override config file values.
|
|
227
|
+
|
|
228
|
+
## How It Works
|
|
229
|
+
|
|
230
|
+
1. **Parsing**, Source files are parsed into an AST using [Acorn](https://github.com/acornjs/acorn)
|
|
231
|
+
2. **Injection**, `await __FlakeMonster__(N)` statements are inserted at statement boundaries inside async function bodies and at the module top level, with marker comments for tracking
|
|
232
|
+
3. **Determinism**, Delay durations are derived from `seed + file + function + position`, so the same seed always produces the same delays
|
|
233
|
+
4. **Removal**, Injected code is removed via text-based pattern matching on the unique stamp (`jt92-se2j!`) and the `__FlakeMonster__` identifier, so it works even after linters, formatters, or AI tools have modified the injected code
|
|
234
|
+
5. **In-place by default**, Injection happens directly in your source files so you can debug freely. Use `--workspace` for isolated copies if preferred
|
|
235
|
+
|
|
236
|
+
## Why Source-to-Source (Not Runtime Hooks)
|
|
237
|
+
|
|
238
|
+
You might wonder why FlakeMonster rewrites files on disk instead of hooking into Node's module loader at runtime.
|
|
239
|
+
|
|
240
|
+
Three reasons:
|
|
241
|
+
|
|
242
|
+
1. **Stable debugging surface**, When a flaky test surfaces, you need to debug it. The injected delays are real code in real files, so you can add `console.log`s, tweak assertions, and iterate freely, the delays stay exactly where they are. A runtime loader would re-inject from scratch on every run, meaning any edit to the file (even adding a log line) shifts injection points and your repro vanishes.
|
|
243
|
+
|
|
244
|
+
2. **Works in the browser**, Not all test suites run in Node. If you're testing with Playwright, Cypress, Vitest browser mode, or anything that executes in a real browser, a Node loader hook is useless. Source-to-source output is just plain JS files, any bundler, dev server, or browser can run them without special integration.
|
|
245
|
+
|
|
246
|
+
3. **Language agnostic**, The core engine knows nothing about JavaScript. Adapters handle parsing and injection per language, and the same workspace/seed/reporting machinery works for all of them. A runtime approach would marry the tool to Node's module system permanently.
|
|
247
|
+
|
|
248
|
+
## Safety
|
|
249
|
+
|
|
250
|
+
- **Unique identifiers**, Injected code uses `__FlakeMonster__` and a stamp (`jt92-se2j!`) that are unmistakable, making false-positive removal essentially impossible
|
|
251
|
+
- **Text-based removal** matches on these unique identifiers, so it works reliably even after linters, formatters, or AI tools rewrite the injected code
|
|
252
|
+
- **Deterministic seeds** mean every failure is reproducible
|
|
253
|
+
- Automatically excludes `node_modules`, `dist`, and `build` directories
|
|
254
|
+
- **Recovery mode**, If traces of injected code remain after a normal restore, `--recover` scans for the stamp and identifier, shows you exactly what it found, and asks for confirmation before removing anything
|
|
255
|
+
|
|
256
|
+
## Recovery Mode
|
|
257
|
+
|
|
258
|
+
Normal restore removes injected code automatically using text-based pattern matching. In rare cases, if some traces of injected code remain after a normal restore, recovery mode lets you interactively inspect and confirm what gets removed.
|
|
259
|
+
|
|
260
|
+
Recovery mode scans for the `jt92-se2j!` stamp, the `__FlakeMonster__` identifier, and the runtime import. It shows you every match before doing anything:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
$ flake-monster restore --recover
|
|
264
|
+
Recovery mode: scanning for injected lines...
|
|
265
|
+
|
|
266
|
+
src/api/users.js (4 matches):
|
|
267
|
+
L3 [import] import { __FlakeMonster__ } from "../flake-monster.runtime.js";
|
|
268
|
+
L7 [stamp] /* @flake-monster[jt92-se2j!] v1 id=a1b2c3d4 seed=921 mode=medium */
|
|
269
|
+
L8 [ident] await __FlakeMonster__.delay({ seed: 921, file: "src/api/users.js", fn: "getUser", n: 0 });
|
|
270
|
+
L12 [stamp] /* @flake-monster[jt92-se2j!] v1 id=e5f6g7h8 seed=921 mode=medium */
|
|
271
|
+
|
|
272
|
+
Total: 4 line(s) across 1 file(s)
|
|
273
|
+
|
|
274
|
+
Remove these lines? (y/N)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
You see exactly which lines will be removed and why (`stamp`, `ident`, or `import`), then confirm before any files are modified.
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flake-monster",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Source-to-source test hardener that injects async delays to surface flaky tests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flake-monster": "./bin/flake-monster.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/unit/*.test.js",
|
|
17
|
+
"test:integration": "node --test test/integration/*.test.js",
|
|
18
|
+
"test:all": "node --test test/**/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"flaky-tests",
|
|
22
|
+
"testing",
|
|
23
|
+
"async",
|
|
24
|
+
"chaos-engineering",
|
|
25
|
+
"test-hardener"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/growthboot/FlakeMonster.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://flakemonster.com",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/growthboot/FlakeMonster/issues"
|
|
35
|
+
},
|
|
36
|
+
"author": "growthboot",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"acorn": "^8.14.0",
|
|
42
|
+
"acorn-walk": "^8.3.4",
|
|
43
|
+
"astravel": "^0.6.1",
|
|
44
|
+
"astring": "^1.9.0",
|
|
45
|
+
"commander": "^13.1.0",
|
|
46
|
+
"fast-glob": "^3.3.3"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Every language adapter must export an object conforming to this shape.
|
|
5
|
+
* The AdapterRegistry validates required methods at registration time.
|
|
6
|
+
*
|
|
7
|
+
* The engine only deals with source text (strings) and metadata objects.
|
|
8
|
+
* All AST parsing, manipulation, and code generation is encapsulated
|
|
9
|
+
* inside the adapter. This is what makes adding a new language possible
|
|
10
|
+
* without touching the core.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} InjectionPoint
|
|
15
|
+
* @property {string} id - Unique ID for this injection (short hex)
|
|
16
|
+
* @property {string} fnName - Name of the containing async function (or "<anonymous>", "<arrow>")
|
|
17
|
+
* @property {number} index - 0-based injection index within this function
|
|
18
|
+
* @property {number} line - 1-based line number in the ORIGINAL source
|
|
19
|
+
* @property {number} column - 0-based column in the ORIGINAL source
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} InjectionResult
|
|
24
|
+
* @property {string} source - The modified source code text
|
|
25
|
+
* @property {InjectionPoint[]} points - Metadata for every injection made
|
|
26
|
+
* @property {boolean} runtimeNeeded - Whether the runtime import was added
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} RemovalResult
|
|
31
|
+
* @property {string} source - The cleaned source code text
|
|
32
|
+
* @property {number} removedCount - How many injections were removed
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} RuntimeInfo
|
|
37
|
+
* @property {string} runtimeSourcePath - Absolute path to the runtime source file
|
|
38
|
+
* @property {string} importStatement - The import/require statement to inject
|
|
39
|
+
* @property {string} runtimeFileName - Filename when copied to workspace
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} InjectOptions
|
|
44
|
+
* @property {string} filePath - Relative file path (for metadata in delay calls)
|
|
45
|
+
* @property {string} mode - "light" | "medium" | "hardcore"
|
|
46
|
+
* @property {number} seed - Integer seed for deterministic delay derivation
|
|
47
|
+
* @property {Object} delayConfig - { minMs, maxMs, distribution }
|
|
48
|
+
* @property {boolean} skipTryCatch - Whether to skip injection inside try/catch/finally
|
|
49
|
+
* @property {boolean} skipGenerators - Whether to skip async generator functions
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {Object} LanguageAdapter
|
|
54
|
+
*
|
|
55
|
+
* @property {string} id
|
|
56
|
+
* Unique identifier, e.g. "javascript", "python", "go".
|
|
57
|
+
*
|
|
58
|
+
* @property {string} displayName
|
|
59
|
+
* Human-readable name, e.g. "JavaScript (ESM)".
|
|
60
|
+
*
|
|
61
|
+
* @property {string[]} fileExtensions
|
|
62
|
+
* Extensions this adapter handles, e.g. [".js", ".mjs"].
|
|
63
|
+
*
|
|
64
|
+
* @property {(filePath: string) => boolean} canHandle
|
|
65
|
+
* Given a file path, return true if this adapter should process it.
|
|
66
|
+
*
|
|
67
|
+
* @property {(source: string, options: InjectOptions) => InjectionResult} inject
|
|
68
|
+
* Parse source text, inject delay statements, return modified source + metadata.
|
|
69
|
+
*
|
|
70
|
+
* @property {(source: string) => RemovalResult} remove
|
|
71
|
+
* Remove all flake-monster injections using text-based pattern matching, return cleaned source.
|
|
72
|
+
*
|
|
73
|
+
* @property {() => RuntimeInfo} getRuntimeInfo
|
|
74
|
+
* Returns info about the runtime file for this language.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/** Required properties that every adapter must have. */
|
|
78
|
+
export const REQUIRED_ADAPTER_PROPERTIES = [
|
|
79
|
+
'id',
|
|
80
|
+
'displayName',
|
|
81
|
+
'fileExtensions',
|
|
82
|
+
'canHandle',
|
|
83
|
+
'inject',
|
|
84
|
+
'remove',
|
|
85
|
+
'getRuntimeInfo',
|
|
86
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { generate } from 'astring';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate source code from an ESTree AST.
|
|
5
|
+
* Preserves comments that were attached by astravel.
|
|
6
|
+
* @param {Object} ast
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function generateSource(ast) {
|
|
10
|
+
return generate(ast, {
|
|
11
|
+
comments: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, join, posix } from 'node:path';
|
|
3
|
+
import { parseSource } from './parser.js';
|
|
4
|
+
import { computeInjections, computeRuntimeImportInsertion, applyInsertions } from './injector.js';
|
|
5
|
+
import { recoverDelays, scanForRecovery } from './remover.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const RUNTIME_FILENAME = 'flake-monster.runtime.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute the relative import path from a file to the runtime at the project root.
|
|
12
|
+
* e.g. for "src/user.js" -> "../flake-monster.runtime.js"
|
|
13
|
+
* for "app.js" -> "./flake-monster.runtime.js"
|
|
14
|
+
* @param {string} filePath - relative file path from project root
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function computeRuntimeImportPath(filePath) {
|
|
18
|
+
// Count directory depth
|
|
19
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
20
|
+
if (parts.length <= 1) {
|
|
21
|
+
// File is at root level
|
|
22
|
+
return `./${RUNTIME_FILENAME}`;
|
|
23
|
+
}
|
|
24
|
+
// Go up (parts.length - 1) directories
|
|
25
|
+
const ups = '../'.repeat(parts.length - 1);
|
|
26
|
+
return `${ups}${RUNTIME_FILENAME}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create the JavaScript language adapter.
|
|
31
|
+
* Handles .js and .mjs files with ESM import/export syntax.
|
|
32
|
+
*
|
|
33
|
+
* @returns {import('../adapter-interface.js').LanguageAdapter}
|
|
34
|
+
*/
|
|
35
|
+
export function createJavaScriptAdapter() {
|
|
36
|
+
return {
|
|
37
|
+
id: 'javascript',
|
|
38
|
+
displayName: 'JavaScript (ESM)',
|
|
39
|
+
fileExtensions: ['.js', '.mjs'],
|
|
40
|
+
|
|
41
|
+
canHandle(filePath) {
|
|
42
|
+
return this.fileExtensions.some((ext) => filePath.endsWith(ext));
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
inject(source, options) {
|
|
46
|
+
const { ast } = parseSource(source);
|
|
47
|
+
const { insertions, points } = computeInjections(ast, source, options);
|
|
48
|
+
const runtimeNeeded = points.length > 0;
|
|
49
|
+
|
|
50
|
+
if (runtimeNeeded) {
|
|
51
|
+
const importPath = computeRuntimeImportPath(options.filePath);
|
|
52
|
+
const imp = computeRuntimeImportInsertion(ast, source, importPath);
|
|
53
|
+
if (imp) insertions.push(imp);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const output = applyInsertions(source, insertions);
|
|
57
|
+
return { source: output, points, runtimeNeeded };
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
remove(source) {
|
|
61
|
+
const { source: cleaned, recoveredCount } = recoverDelays(source);
|
|
62
|
+
return { source: cleaned, removedCount: recoveredCount };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
scan(source) {
|
|
66
|
+
return scanForRecovery(source);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
getRuntimeInfo() {
|
|
70
|
+
return {
|
|
71
|
+
runtimeSourcePath: join(__dirname, '..', '..', 'runtime', 'javascript', RUNTIME_FILENAME),
|
|
72
|
+
runtimeFileName: RUNTIME_FILENAME,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|