Skip to content

Commit 8a4ea41

Browse files
authored
refactor: type pipeline core (#217)
1 parent 45cee57 commit 8a4ea41

File tree

7 files changed

+97
-65
lines changed

7 files changed

+97
-65
lines changed

src/pipeline/executor.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import { getStep, type StepHandler } from './registry.js';
88
import { log } from '../logger.js';
99

1010
export interface PipelineContext {
11-
args?: Record<string, any>;
11+
args?: Record<string, unknown>;
1212
debug?: boolean;
1313
}
1414

1515
export async function executePipeline(
1616
page: IPage | null,
17-
pipeline: any[],
17+
pipeline: unknown[],
1818
ctx: PipelineContext = {},
19-
): Promise<any> {
19+
): Promise<unknown> {
2020
const args = ctx.args ?? {};
2121
const debug = ctx.debug ?? false;
22-
let data: any = null;
22+
let data: unknown = null;
2323
const total = pipeline.length;
2424

2525
for (let i = 0; i < pipeline.length; i++) {
@@ -41,7 +41,7 @@ export async function executePipeline(
4141
return data;
4242
}
4343

44-
function debugStepStart(stepNum: number, total: number, op: string, params: any): void {
44+
function debugStepStart(stepNum: number, total: number, op: string, params: unknown): void {
4545
let preview = '';
4646
if (typeof params === 'string') {
4747
preview = params.length <= 80 ? ` → ${params}` : ` → ${params.slice(0, 77)}...`;
@@ -51,7 +51,7 @@ function debugStepStart(stepNum: number, total: number, op: string, params: any)
5151
log.step(stepNum, total, op, preview);
5252
}
5353

54-
function debugStepResult(op: string, data: any): void {
54+
function debugStepResult(op: string, data: unknown): void {
5555
if (data === null || data === undefined) {
5656
log.stepResult('(no data)');
5757
} else if (Array.isArray(data)) {

src/pipeline/registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import { stepDownload } from './steps/download.js';
1818
* TData is the type of the `data` state flowing into the step.
1919
* TResult is the expected return type.
2020
*/
21-
export type StepHandler<TData = any, TResult = any> = (
21+
export type StepHandler<TData = unknown, TResult = unknown, TParams = unknown> = (
2222
page: IPage | null,
23-
params: any,
23+
params: TParams,
2424
data: TData,
25-
args: Record<string, any>
25+
args: Record<string, unknown>
2626
) => Promise<TResult>;
2727

2828
const _stepRegistry = new Map<string, StepHandler>();

src/pipeline/steps/browser.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,28 @@
66
import type { IPage } from '../../types.js';
77
import { render } from '../template.js';
88

9-
export async function stepNavigate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
10-
if (typeof params === 'object' && params && 'url' in params) {
9+
function isRecord(value: unknown): value is Record<string, unknown> {
10+
return typeof value === 'object' && value !== null && !Array.isArray(value);
11+
}
12+
13+
export async function stepNavigate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
14+
if (isRecord(params) && 'url' in params) {
1115
const url = String(render(params.url, { args, data }));
12-
await page!.goto(url, { waitUntil: params.waitUntil, settleMs: params.settleMs });
16+
await page!.goto(url, { waitUntil: params.waitUntil as 'load' | 'none' | undefined, settleMs: typeof params.settleMs === 'number' ? params.settleMs : undefined });
1317
} else {
1418
const url = render(params, { args, data });
1519
await page!.goto(String(url));
1620
}
1721
return data;
1822
}
1923

20-
export async function stepClick(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
24+
export async function stepClick(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
2125
await page!.click(String(render(params, { args, data })).replace(/^@/, ''));
2226
return data;
2327
}
2428

25-
export async function stepType(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
26-
if (typeof params === 'object' && params) {
29+
export async function stepType(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
30+
if (isRecord(params)) {
2731
const ref = String(render(params.ref ?? '', { args, data })).replace(/^@/, '');
2832
const text = String(render(params.text ?? '', { args, data }));
2933
await page!.typeText(ref, text);
@@ -32,32 +36,37 @@ export async function stepType(page: IPage | null, params: any, data: any, args:
3236
return data;
3337
}
3438

35-
export async function stepWait(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
39+
export async function stepWait(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
3640
if (typeof params === 'number') await page!.wait(params);
37-
else if (typeof params === 'object' && params) {
41+
else if (isRecord(params)) {
3842
if ('text' in params) {
3943
await page!.wait({
4044
text: String(render(params.text, { args, data })),
41-
timeout: params.timeout
45+
timeout: typeof params.timeout === 'number' ? params.timeout : undefined,
4246
});
4347
} else if ('time' in params) await page!.wait(Number(params.time));
4448
} else if (typeof params === 'string') await page!.wait(Number(render(params, { args, data })));
4549
return data;
4650
}
4751

48-
export async function stepPress(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
52+
export async function stepPress(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
4953
await page!.pressKey(String(render(params, { args, data })));
5054
return data;
5155
}
5256

53-
export async function stepSnapshot(page: IPage | null, params: any, _data: any, _args: Record<string, any>): Promise<any> {
54-
const opts = (typeof params === 'object' && params) ? params : {};
55-
return page!.snapshot({ interactive: opts.interactive ?? false, compact: opts.compact ?? false, maxDepth: opts.max_depth, raw: opts.raw ?? false });
57+
export async function stepSnapshot(page: IPage | null, params: unknown, _data: unknown, _args: Record<string, unknown>): Promise<unknown> {
58+
const opts = isRecord(params) ? params : {};
59+
return page!.snapshot({
60+
interactive: typeof opts.interactive === 'boolean' ? opts.interactive : false,
61+
compact: typeof opts.compact === 'boolean' ? opts.compact : false,
62+
maxDepth: typeof opts.max_depth === 'number' ? opts.max_depth : undefined,
63+
raw: typeof opts.raw === 'boolean' ? opts.raw : false,
64+
});
5665
}
5766

58-
export async function stepEvaluate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
67+
export async function stepEvaluate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
5968
const js = String(render(params, { args, data }));
60-
let result = await page!.evaluate(js);
69+
let result: unknown = await page!.evaluate(js);
6170
// MCP may return JSON as a string — auto-parse it
6271
if (typeof result === 'string') {
6372
const trimmed = result.trim();

src/pipeline/steps/fetch.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import type { IPage } from '../../types.js';
66
import { render } from '../template.js';
77

8+
function isRecord(value: unknown): value is Record<string, unknown> {
9+
return typeof value === 'object' && value !== null && !Array.isArray(value);
10+
}
11+
812
/** Simple async concurrency limiter */
913
async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
1014
const results: R[] = new Array(items.length);
@@ -25,9 +29,9 @@ async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, inde
2529
/** Single URL fetch helper */
2630
async function fetchSingle(
2731
page: IPage | null, url: string, method: string,
28-
queryParams: Record<string, any>, headers: Record<string, any>,
29-
args: Record<string, any>, data: any,
30-
): Promise<any> {
32+
queryParams: Record<string, unknown>, headers: Record<string, unknown>,
33+
args: Record<string, unknown>, data: unknown,
34+
): Promise<unknown> {
3135
const renderedParams: Record<string, string> = {};
3236
for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
3337
const renderedHeaders: Record<string, string> = {};
@@ -65,7 +69,7 @@ async function fetchSingle(
6569
async function fetchBatchInBrowser(
6670
page: IPage, urls: string[], method: string,
6771
headers: Record<string, string>, concurrency: number,
68-
): Promise<any[]> {
72+
): Promise<unknown[]> {
6973
const headersJs = JSON.stringify(headers);
7074
const urlsJs = JSON.stringify(urls);
7175
return page.evaluate(`
@@ -97,24 +101,25 @@ async function fetchBatchInBrowser(
97101
`);
98102
}
99103

100-
export async function stepFetch(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
101-
const urlOrObj = typeof params === 'string' ? params : (params?.url ?? '');
102-
const method = params?.method ?? 'GET';
103-
const queryParams: Record<string, any> = params?.params ?? {};
104-
const headers: Record<string, any> = params?.headers ?? {};
104+
export async function stepFetch(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
105+
const paramObject = isRecord(params) ? params : {};
106+
const urlOrObj = typeof params === 'string' ? params : (paramObject.url ?? '');
107+
const method = typeof paramObject.method === 'string' ? paramObject.method : 'GET';
108+
const queryParams = isRecord(paramObject.params) ? paramObject.params : {};
109+
const headers = isRecord(paramObject.headers) ? paramObject.headers : {};
105110
const urlTemplate = String(urlOrObj);
106111

107112
// Per-item fetch when data is array and URL references item
108113
if (Array.isArray(data) && urlTemplate.includes('item')) {
109-
const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 5;
114+
const concurrency = typeof paramObject.concurrency === 'number' ? paramObject.concurrency : 5;
110115

111116
// Render all URLs upfront
112117
const renderedHeaders: Record<string, string> = {};
113118
for (const [k, v] of Object.entries(headers)) renderedHeaders[k] = String(render(v, { args, data }));
114119
const renderedParams: Record<string, string> = {};
115120
for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
116121

117-
const urls = data.map((item: any, index: number) => {
122+
const urls = data.map((item, index) => {
118123
let url = String(render(urlTemplate, { args, data, item, index }));
119124
if (Object.keys(renderedParams).length > 0) {
120125
const qs = new URLSearchParams(renderedParams).toString();

src/pipeline/steps/transform.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
* Pipeline steps: data transforms — select, map, filter, sort, limit.
33
*/
44

5+
import type { IPage } from '../../types.js';
56
import { render, evalExpr } from '../template.js';
67

7-
export async function stepSelect(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
8+
function isRecord(value: unknown): value is Record<string, unknown> {
9+
return typeof value === 'object' && value !== null && !Array.isArray(value);
10+
}
11+
12+
export async function stepSelect(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
813
const pathStr = String(render(params, { args, data }));
914
if (data && typeof data === 'object') {
10-
let current = data;
15+
let current: unknown = data;
1116
for (const part of pathStr.split('.')) {
12-
if (current && typeof current === 'object' && !Array.isArray(current)) current = (current as any)[part];
17+
if (isRecord(current)) current = current[part];
1318
else if (Array.isArray(current) && /^\d+$/.test(part)) current = current[parseInt(part, 10)];
1419
else return null;
1520
}
@@ -18,24 +23,25 @@ export async function stepSelect(_page: any, params: any, data: any, args: Recor
1823
return data;
1924
}
2025

21-
export async function stepMap(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
26+
export async function stepMap(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
2227
if (!data || typeof data !== 'object') return data;
23-
let source = data;
28+
let source: unknown = data;
2429

2530
// Support inline select: { map: { select: 'path', key: '${{ item.x }}' } }
26-
if (params && typeof params === 'object' && 'select' in params) {
27-
source = await stepSelect(null, (params as any).select, data, args);
31+
if (isRecord(params) && 'select' in params) {
32+
source = await stepSelect(null, params.select, data, args);
2833
}
2934

3035
if (!source || typeof source !== 'object') return source;
3136

32-
let items: any[] = Array.isArray(source) ? source : [source];
33-
if (!Array.isArray(source) && typeof source === 'object' && 'data' in source) items = source.data;
34-
const result: any[] = [];
37+
let items: unknown[] = Array.isArray(source) ? source : [source];
38+
if (isRecord(source) && Array.isArray(source.data)) items = source.data;
39+
const result: Array<Record<string, unknown>> = [];
40+
const templateParams = isRecord(params) ? params : {};
3541
for (let i = 0; i < items.length; i++) {
3642
const item = items[i];
37-
const row: Record<string, any> = {};
38-
for (const [key, template] of Object.entries(params)) {
43+
const row: Record<string, unknown> = {};
44+
for (const [key, template] of Object.entries(templateParams)) {
3945
if (key === 'select') continue;
4046
row[key] = render(template, { args, data: source, item, index: i });
4147
}
@@ -44,19 +50,26 @@ export async function stepMap(_page: any, params: any, data: any, args: Record<s
4450
return result;
4551
}
4652

47-
export async function stepFilter(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
53+
export async function stepFilter(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
4854
if (!Array.isArray(data)) return data;
4955
return data.filter((item, i) => evalExpr(String(params), { args, item, index: i }));
5056
}
5157

52-
export async function stepSort(_page: any, params: any, data: any, _args: Record<string, any>): Promise<any> {
58+
export async function stepSort(_page: IPage | null, params: unknown, data: unknown, _args: Record<string, unknown>): Promise<unknown> {
5359
if (!Array.isArray(data)) return data;
54-
const key = typeof params === 'object' ? (params.by ?? '') : String(params);
55-
const reverse = typeof params === 'object' ? params.order === 'desc' : false;
56-
return [...data].sort((a, b) => { const va = a[key] ?? ''; const vb = b[key] ?? ''; const cmp = va < vb ? -1 : va > vb ? 1 : 0; return reverse ? -cmp : cmp; });
60+
const key = isRecord(params) ? String(params.by ?? '') : String(params);
61+
const reverse = isRecord(params) ? params.order === 'desc' : false;
62+
return [...data].sort((a, b) => {
63+
const left = isRecord(a) ? a[key] : undefined;
64+
const right = isRecord(b) ? b[key] : undefined;
65+
const va = left ?? '';
66+
const vb = right ?? '';
67+
const cmp = va < vb ? -1 : va > vb ? 1 : 0;
68+
return reverse ? -cmp : cmp;
69+
});
5770
}
5871

59-
export async function stepLimit(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
72+
export async function stepLimit(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
6073
if (!Array.isArray(data)) return data;
6174
return data.slice(0, Number(render(params, { args, data })));
6275
}

src/pipeline/template.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
*/
44

55
export interface RenderContext {
6-
args?: Record<string, any>;
7-
data?: any;
8-
item?: any;
6+
args?: Record<string, unknown>;
7+
data?: unknown;
8+
item?: unknown;
99
index?: number;
1010
}
1111

12-
export function render(template: any, ctx: RenderContext): any {
12+
function isRecord(value: unknown): value is Record<string, unknown> {
13+
return typeof value === 'object' && value !== null && !Array.isArray(value);
14+
}
15+
16+
export function render(template: unknown, ctx: RenderContext): unknown {
1317
if (typeof template !== 'string') return template;
1418
const trimmed = template.trim();
1519
// Full expression: entire string is a single ${{ ... }}
@@ -26,7 +30,7 @@ export function render(template: any, ctx: RenderContext): any {
2630
return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
2731
}
2832

29-
export function evalExpr(expr: string, ctx: RenderContext): any {
33+
export function evalExpr(expr: string, ctx: RenderContext): unknown {
3034
const args = ctx.args ?? {};
3135
const item = ctx.item ?? {};
3236
const data = ctx.data;
@@ -81,7 +85,7 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
8185
* default(val), join(sep), upper, lower, truncate(n), trim,
8286
* replace(old,new), keys, length, first, last, json
8387
*/
84-
function applyFilter(filterExpr: string, value: any): any {
88+
function applyFilter(filterExpr: string, value: unknown): unknown {
8589
const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
8690
if (!match) return value;
8791
const [, name, rawArgs] = match;
@@ -158,21 +162,22 @@ function applyFilter(filterExpr: string, value: any): any {
158162
}
159163
}
160164

161-
export function resolvePath(pathStr: string, ctx: RenderContext): any {
165+
export function resolvePath(pathStr: string, ctx: RenderContext): unknown {
162166
const args = ctx.args ?? {};
163167
const item = ctx.item ?? {};
164168
const data = ctx.data;
165169
const index = ctx.index ?? 0;
166170
const parts = pathStr.split('.');
167171
const rootName = parts[0];
168-
let obj: any; let rest: string[];
172+
let obj: unknown;
173+
let rest: string[];
169174
if (rootName === 'args') { obj = args; rest = parts.slice(1); }
170175
else if (rootName === 'item') { obj = item; rest = parts.slice(1); }
171176
else if (rootName === 'data') { obj = data; rest = parts.slice(1); }
172177
else if (rootName === 'index') return index;
173178
else { obj = item; rest = parts; }
174179
for (const part of rest) {
175-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) obj = obj[part];
180+
if (isRecord(obj)) obj = obj[part];
176181
else if (Array.isArray(obj) && /^\d+$/.test(part)) obj = obj[parseInt(part, 10)];
177182
else return null;
178183
}
@@ -190,7 +195,7 @@ export function resolvePath(pathStr: string, ctx: RenderContext): any {
190195
* If opencli ever loads untrusted third-party adapters, this MUST be replaced
191196
* with a proper sandboxed evaluator.
192197
*/
193-
function evalJsExpr(expr: string, ctx: RenderContext): any {
198+
function evalJsExpr(expr: string, ctx: RenderContext): unknown {
194199
// Guard against absurdly long expressions that could indicate injection.
195200
if (expr.length > 2000) return undefined;
196201

src/pipeline/transform.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ describe('stepFilter', () => {
8888
describe('stepSort', () => {
8989
it('sorts ascending by key', async () => {
9090
const result = await stepSort(null, 'score', SAMPLE_DATA, {});
91-
expect(result.map((r: any) => r.title)).toEqual(['Alpha', 'Gamma', 'Beta']);
91+
expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Alpha', 'Gamma', 'Beta']);
9292
});
9393

9494
it('sorts descending', async () => {
9595
const result = await stepSort(null, { by: 'score', order: 'desc' }, SAMPLE_DATA, {});
96-
expect(result.map((r: any) => r.title)).toEqual(['Beta', 'Gamma', 'Alpha']);
96+
expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Beta', 'Gamma', 'Alpha']);
9797
});
9898

9999
it('does not mutate original', async () => {

0 commit comments

Comments
 (0)