tap.ts
1 /** 2 * Pipeline step: tap — declarative Store Action Bridge. 3 * 4 * Generates a self-contained IIFE that: 5 * 1. Injects fetch + XHR dual interception proxy 6 * 2. Finds the Pinia/Vuex store and calls the action 7 * 3. Captures the response matching the URL pattern 8 * 4. Auto-cleans up interception in finally block 9 * 5. Returns the captured data (optionally sub-selected) 10 */ 11 12 import type { IPage } from '../../types.js'; 13 import { render } from '../template.js'; 14 import { generateTapInterceptorJs } from '../../interceptor.js'; 15 import { CliError } from '../../errors.js'; 16 17 interface TapParams { 18 store?: string; 19 action?: string; 20 capture?: string; 21 timeout?: number; 22 select?: string; 23 framework?: string | null; 24 args?: unknown[]; 25 } 26 27 export async function stepTap( 28 page: IPage | null, 29 params: unknown, 30 data: unknown, 31 args: Record<string, unknown>, 32 ): Promise<unknown> { 33 const cfg: TapParams = typeof params === 'object' && params !== null ? (params as TapParams) : {}; 34 const storeName = String(render(cfg.store ?? '', { args, data })); 35 const actionName = String(render(cfg.action ?? '', { args, data })); 36 const capturePattern = String(render(cfg.capture ?? '', { args, data })); 37 const timeout = cfg.timeout ?? 5; 38 const selectPath = cfg.select ? String(render(cfg.select, { args, data })) : null; 39 const framework = cfg.framework ?? null; 40 const actionArgs: unknown[] = cfg.args ?? []; 41 42 if (!storeName || !actionName) throw new CliError('TAP_MISSING_PARAMS', 'tap: store and action are required'); 43 44 // Build select chain for the captured response 45 const selectChain = selectPath 46 ? selectPath.split('.').map((p: string) => `?.[${JSON.stringify(p)}]`).join('') 47 : ''; 48 49 // Serialize action arguments 50 const actionArgsRendered = actionArgs.map((a) => { 51 const rendered = render(a, { args, data }); 52 return JSON.stringify(rendered); 53 }); 54 const actionCall = actionArgsRendered.length 55 ? `store[${JSON.stringify(actionName)}](${actionArgsRendered.join(', ')})` 56 : `store[${JSON.stringify(actionName)}]()`; 57 58 // Use shared interceptor generator for fetch/XHR patching 59 const tap = generateTapInterceptorJs(JSON.stringify(capturePattern)); 60 61 const js = ` 62 async () => { 63 // ── 1. Setup capture proxy (fetch + XHR dual interception) ── 64 ${tap.setupVar} 65 ${tap.fetchPatch} 66 ${tap.xhrPatch} 67 68 try { 69 // ── 2. Find store ── 70 let store = null; 71 const storeName = ${JSON.stringify(storeName)}; 72 const fw = ${JSON.stringify(framework)}; 73 74 const app = document.querySelector('#app'); 75 if (!fw || fw === 'pinia') { 76 try { 77 const pinia = app?.__vue_app__?.config?.globalProperties?.$pinia; 78 if (pinia?._s) store = pinia._s.get(storeName); 79 } catch {} 80 } 81 if (!store && (!fw || fw === 'vuex')) { 82 try { 83 const vuexStore = app?.__vue_app__?.config?.globalProperties?.$store 84 ?? app?.__vue__?.$store; 85 if (vuexStore) { 86 store = { [${JSON.stringify(actionName)}]: (...a) => vuexStore.dispatch(storeName + '/' + ${JSON.stringify(actionName)}, ...a) }; 87 } 88 } catch {} 89 } 90 91 if (!store) return { error: 'Store not found: ' + storeName, hint: 'Page may not be fully loaded or store name may be incorrect' }; 92 if (typeof store[${JSON.stringify(actionName)}] !== 'function') { 93 return { error: 'Action not found: ' + ${JSON.stringify(actionName)} + ' on store ' + storeName, 94 hint: 'Available: ' + Object.keys(store).filter(k => typeof store[k] === 'function' && !k.startsWith('$') && !k.startsWith('_')).join(', ') }; 95 } 96 97 // ── 3. Call store action ── 98 await ${actionCall}; 99 100 // ── 4. Wait for network response ── 101 if (!${tap.capturedVar}) { 102 const timeoutPromise = new Promise(r => setTimeout(r, ${timeout} * 1000)); 103 await Promise.race([${tap.promiseVar}, timeoutPromise]); 104 } 105 } finally { 106 // ── 5. Always restore originals ── 107 ${tap.restorePatch} 108 } 109 110 if (!${tap.capturedVar}) return { error: 'No matching response captured for pattern: ' + capturePattern }; 111 return ${tap.capturedVar}${selectChain} ?? ${tap.capturedVar}; 112 } 113 `; 114 115 return page!.evaluate(js); 116 }