Skip to content

Commit 6fbb602

Browse files
authored
[32] Refactor from "replacement processors" to "engines" & add AsyncLookaheadTransformEngine (#38)
* Refactor to "engines" from "replacement processors" * Add codemod for engines * Add async lookahead engine and related concurrency strategies * prepublishOnly -> prepack * added note re: contrived search strategy to ensure regex construction cost to regex callback benchmark strategy
1 parent a147486 commit 6fbb602

128 files changed

Lines changed: 7306 additions & 3461 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 126 additions & 121 deletions
Large diffs are not rendered by default.

codemods/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ This directory contains migration codemods for breaking API changes.
44

55
## Available Codemods
66

7-
### v1 -> v2: [`replacement-callback-positional-to-context`](./transforms/v1-v2/README.md)
7+
### v1 -> v2: [two-step migration](./transforms/v1-v2/README.md)
8+
9+
Run in order:
10+
11+
1. `replacement-callback-positional-to-context` — migrates callback signatures from positional args to a context object
12+
2. `processor-to-engine` — renames processor classes to engines, moves `stopReplacingSignal`, strips dropped type parameters

codemods/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"type": "module",
77
"scripts": {
88
"codemod:replacement-callback-context": "jscodeshift -t transforms/v1-v2/replacement-callback-positional-to-context.js --parser=tsx --extensions=js,jsx,ts,tsx,mjs",
9+
"codemod:processor-to-engine": "jscodeshift -t transforms/v1-v2/processor-to-engine.js --parser=tsx --extensions=js,jsx,ts,tsx,mjs",
910
"test": "vitest run"
1011
},
1112
"devDependencies": {

codemods/transforms/v1-v2/README.md

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# `replacement-callback-positional-to-context`
1+
# v1 → v2 migration codemods
2+
3+
Run these in order. Each transform is idempotent, so re-running a step that has already been applied is safe.
4+
5+
## Step 1 — `replacement-callback-positional-to-context`
26

37
Transforms replacement callback signatures from positional arguments to a context-object form.
48

@@ -16,8 +20,6 @@ replacement: (match, { matchIndex }) => `#${matchIndex}: ${match}`
1620
replacement: (match, { matchIndex, streamIndices }) => `${streamIndices[0]}-${streamIndices[1]}`
1721
```
1822

19-
## Usage
20-
2123
Dry run:
2224

2325
```bash
@@ -30,15 +32,82 @@ Apply changes:
3032
npm run codemod:replacement-callback-context -- src
3133
```
3234

33-
Narrow by extensions if needed:
35+
### Notes
36+
37+
- Only updates inline function/arrow callbacks under a `replacement` property.
38+
- Existing context-object signatures are left unchanged.
39+
- If the third positional arg is destructured inline (for example, `[startIndex, endIndex]`), the transform intentionally skips that callback for manual migration.
40+
- Non-standard callback signatures are skipped for safety and should be migrated manually.
41+
42+
---
43+
44+
## Step 2 — `processor-to-engine`
45+
46+
Renames the deprecated processor classes to their engine equivalents, moves `stopReplacingSignal` from the adapter constructor into the engine options when the engine is inlined, and strips the now-dropped type parameter from adapter constructors.
47+
48+
| Old | New |
49+
|---|---|
50+
| `StaticReplacementProcessor` | `SyncReplacementTransformEngine` |
51+
| `FunctionReplacementProcessor` | `SyncReplacementTransformEngine` |
52+
| `IterableFunctionReplacementProcessor` | `SyncReplacementTransformEngine` |
53+
| `AsyncFunctionReplacementProcessor` | `AsyncSerialReplacementTransformEngine` |
54+
| `AsyncIterableFunctionReplacementProcessor` | `AsyncSerialReplacementTransformEngine` |
55+
56+
Before:
57+
58+
```ts
59+
import { FunctionReplacementProcessor } from "replace-content-transformer";
60+
61+
const engine = new FunctionReplacementProcessor({
62+
searchStrategy,
63+
replacement: (match, { matchIndex }) => `${matchIndex}: ${match}`,
64+
});
65+
66+
const transformer = new ReplaceContentTransformer<Promise<string>>(engine, abortController.signal);
67+
```
68+
69+
After:
70+
71+
```ts
72+
import { SyncReplacementTransformEngine } from "replace-content-transformer";
73+
74+
const engine = new SyncReplacementTransformEngine({
75+
searchStrategy,
76+
replacement: (match, { matchIndex }) => `${matchIndex}: ${match}`,
77+
});
78+
79+
const transformer = new ReplaceContentTransformer(engine);
80+
```
81+
82+
When the engine is constructed inline in the adapter call, `stopReplacingSignal` is moved automatically:
83+
84+
```ts
85+
// Before
86+
const t = new ReplaceContentTransformer(
87+
new FunctionReplacementProcessor({ searchStrategy, replacement }),
88+
abortController.signal
89+
);
90+
91+
// After
92+
const t = new ReplaceContentTransformer(
93+
new SyncReplacementTransformEngine({ searchStrategy, replacement, stopReplacingSignal: abortController.signal })
94+
);
95+
```
96+
97+
Dry run:
3498

3599
```bash
36-
npm run codemod:replacement-callback-context -- --extensions=ts,tsx "src/**/*.ts"
100+
npm run codemod:processor-to-engine -- --dry --print src
37101
```
38102

39-
## Notes
103+
Apply changes:
40104

41-
- The transform only updates inline function/arrow callbacks under a `replacement` property.
42-
- Existing context-object signatures are left unchanged.
43-
- If the third positional arg is destructured inline (for example, `[startIndex, endIndex]`), the transform intentionally skips that callback for manual migration.
44-
- Non-standard callback signatures are skipped for safety and should be migrated manually.
105+
```bash
106+
npm run codemod:processor-to-engine -- src
107+
```
108+
109+
### Notes
110+
111+
- Aliased imports (`import { FunctionReplacementProcessor as FRP }`) are not renamed; update the alias and its usages manually.
112+
- When `stopReplacingSignal` is held in a variable and passed by reference (not inline), it cannot be moved automatically — TypeScript will surface the remaining mismatch as a type error.
113+
- If you previously used `AsyncFunctionReplacementProcessor` or `AsyncIterableFunctionReplacementProcessor` and want concurrent lookahead semantics, migrate to `AsyncLookaheadTransformEngine` manually; the codemod maps to the conservative `AsyncSerialReplacementTransformEngine` which preserves identical serial behaviour.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Codemod: replacement processors -> transform engines (v1 -> v2)
3+
*
4+
* Run this AFTER replacement-callback-positional-to-context.
5+
*
6+
* Transforms:
7+
* 1. Renames the five deprecated processor classes to their engine equivalents
8+
* in both `new` expressions and named import specifiers.
9+
* 2. Deduplicates import specifiers when multiple processors map to the same
10+
* engine name (e.g. FunctionReplacementProcessor and StaticReplacementProcessor
11+
* both become SyncReplacementTransformEngine).
12+
* 3. When an engine is constructed inline as the first argument of an adapter
13+
* constructor and a stopReplacingSignal is passed as the second argument,
14+
* moves the signal into the engine options object and removes it from the
15+
* adapter call.
16+
* 4. Strips the now-dropped type parameter from adapter constructors
17+
* (e.g. new ReplaceContentTransformer<Promise<string>>(...)).
18+
*
19+
* Processor -> Engine mapping:
20+
* StaticReplacementProcessor -> SyncReplacementTransformEngine
21+
* FunctionReplacementProcessor -> SyncReplacementTransformEngine
22+
* IterableFunctionReplacementProcessor -> SyncReplacementTransformEngine
23+
* AsyncFunctionReplacementProcessor -> AsyncSerialReplacementTransformEngine
24+
* AsyncIterableFunctionReplacementProcessor -> AsyncSerialReplacementTransformEngine
25+
*
26+
* Limitations:
27+
* - Aliased imports (import { FunctionReplacementProcessor as FRP }) are not
28+
* renamed; the local alias and all its usages must be updated manually.
29+
* - When stopReplacingSignal is held in a variable rather than inlined,
30+
* it cannot be moved automatically; TypeScript will surface the remaining
31+
* mismatch after migration.
32+
*/
33+
34+
const PROCESSOR_TO_ENGINE = new Map([
35+
["StaticReplacementProcessor", "SyncReplacementTransformEngine"],
36+
["FunctionReplacementProcessor", "SyncReplacementTransformEngine"],
37+
["IterableFunctionReplacementProcessor", "SyncReplacementTransformEngine"],
38+
["AsyncFunctionReplacementProcessor", "AsyncSerialReplacementTransformEngine"],
39+
[
40+
"AsyncIterableFunctionReplacementProcessor",
41+
"AsyncSerialReplacementTransformEngine",
42+
],
43+
]);
44+
45+
const ADAPTER_NAMES = new Set([
46+
"ReplaceContentTransformer",
47+
"AsyncReplaceContentTransformer",
48+
"ReplaceContentTransform",
49+
"AsyncReplaceContentTransform",
50+
]);
51+
52+
function identifierName(node) {
53+
return node?.type === "Identifier" ? node.name : null;
54+
}
55+
56+
export default function processorToEngine(fileInfo, api) {
57+
const j = api.jscodeshift;
58+
const root = j(fileInfo.source);
59+
let changed = false;
60+
61+
// 1. Rename processor import specifiers
62+
root.find(j.ImportSpecifier).forEach((path) => {
63+
const name = identifierName(path.node.imported);
64+
if (!name) return;
65+
const engineName = PROCESSOR_TO_ENGINE.get(name);
66+
if (!engineName) return;
67+
68+
const isShorthand = path.node.local.name === name;
69+
path.node.imported = j.identifier(engineName);
70+
if (isShorthand) {
71+
path.node.local = j.identifier(engineName);
72+
}
73+
changed = true;
74+
});
75+
76+
// Deduplicate specifiers: multiple processors may collapse to the same engine name
77+
root.find(j.ImportDeclaration).forEach((path) => {
78+
const before = path.node.specifiers.length;
79+
const seen = new Set();
80+
path.node.specifiers = path.node.specifiers.filter((spec) => {
81+
if (spec.type !== "ImportSpecifier") return true;
82+
const key = `${identifierName(spec.imported)}:${spec.local.name}`;
83+
if (seen.has(key)) return false;
84+
seen.add(key);
85+
return true;
86+
});
87+
if (path.node.specifiers.length !== before) changed = true;
88+
});
89+
90+
// 2. Rename new XxxProcessor(...) expressions
91+
root
92+
.find(j.NewExpression, (node) => {
93+
const name = identifierName(node.callee);
94+
return name != null && PROCESSOR_TO_ENGINE.has(name);
95+
})
96+
.forEach((path) => {
97+
path.node.callee = j.identifier(
98+
PROCESSOR_TO_ENGINE.get(path.node.callee.name)
99+
);
100+
changed = true;
101+
});
102+
103+
// 3. Move inline stopReplacingSignal from adapter 2nd arg into engine options
104+
// Matches: new Adapter(new Engine({ ... }), signal)
105+
// Produces: new Adapter(new Engine({ ..., stopReplacingSignal: signal }))
106+
root
107+
.find(j.NewExpression, (node) => {
108+
const name = identifierName(node.callee);
109+
return (
110+
name != null &&
111+
ADAPTER_NAMES.has(name) &&
112+
node.arguments.length === 2 &&
113+
node.arguments[0]?.type === "NewExpression"
114+
);
115+
})
116+
.forEach((path) => {
117+
const [engineNew, signalArg] = path.node.arguments;
118+
const engineArgs = engineNew.arguments;
119+
120+
if (
121+
engineArgs.length !== 1 ||
122+
engineArgs[0].type !== "ObjectExpression"
123+
) {
124+
return;
125+
}
126+
127+
engineArgs[0].properties.push(
128+
j.property(
129+
"init",
130+
j.identifier("stopReplacingSignal"),
131+
signalArg
132+
)
133+
);
134+
path.node.arguments = [engineNew];
135+
changed = true;
136+
});
137+
138+
// 4. Strip type parameters from adapter constructors
139+
// e.g. new ReplaceContentTransformer<Promise<string>>(...) -> new ReplaceContentTransformer(...)
140+
root
141+
.find(j.NewExpression, (node) => {
142+
const name = identifierName(node.callee);
143+
return name != null && ADAPTER_NAMES.has(name) && node.typeParameters != null;
144+
})
145+
.forEach((path) => {
146+
path.node.typeParameters = null;
147+
changed = true;
148+
});
149+
150+
if (!changed) return null;
151+
return root.toSource();
152+
}
153+
154+
export const parser = "tsx";

0 commit comments

Comments
 (0)