slapify 0.0.14 → 0.0.17
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/README.md +342 -258
- package/dist/ai/interpreter.d.ts +13 -0
- package/dist/ai/interpreter.js +1 -293
- package/dist/browser/agent.js +1 -485
- package/dist/cli.js +1 -1315
- package/dist/config/loader.js +1 -305
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -260
- package/dist/parser/flow.js +1 -117
- package/dist/perf/audit.d.ts +215 -0
- package/dist/perf/audit.js +1 -0
- package/dist/report/generator.d.ts +1 -0
- package/dist/report/generator.js +1 -549
- package/dist/runner/index.d.ts +14 -1
- package/dist/runner/index.js +1 -584
- package/dist/task/index.d.ts +5 -0
- package/dist/task/index.js +1 -0
- package/dist/task/report.d.ts +9 -0
- package/dist/task/report.js +1 -0
- package/dist/task/runner.d.ts +3 -0
- package/dist/task/runner.js +1 -0
- package/dist/task/session.d.ts +18 -0
- package/dist/task/session.js +1 -0
- package/dist/task/tools.d.ts +253 -0
- package/dist/task/tools.js +1 -0
- package/dist/task/types.d.ts +153 -0
- package/dist/task/types.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -2
- package/package.json +25 -15
- package/dist/ai/interpreter.d.ts.map +0 -1
- package/dist/ai/interpreter.js.map +0 -1
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parser/flow.d.ts.map +0 -1
- package/dist/parser/flow.js.map +0 -1
- package/dist/report/generator.d.ts.map +0 -1
- package/dist/report/generator.js.map +0 -1
- package/dist/runner/index.d.ts.map +0 -1
- package/dist/runner/index.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/parser/flow.js
CHANGED
|
@@ -1,117 +1 @@
|
|
|
1
|
-
import fs from "
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { glob } from "glob";
|
|
4
|
-
/**
|
|
5
|
-
* Parse a single line into a FlowStep
|
|
6
|
-
*/
|
|
7
|
-
function parseLine(line, lineNumber) {
|
|
8
|
-
const trimmed = line.trim();
|
|
9
|
-
// Skip empty lines and comments
|
|
10
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
// Check for [Optional] prefix
|
|
14
|
-
const optionalMatch = trimmed.match(/^\[Optional\]\s*(.+)$/i);
|
|
15
|
-
const isOptional = !!optionalMatch;
|
|
16
|
-
const stepText = optionalMatch ? optionalMatch[1] : trimmed;
|
|
17
|
-
// Check for conditional (If ... appears, ...)
|
|
18
|
-
const conditionalMatch = stepText.match(/^If\s+(.+?)\s*,\s*(.+)$/i);
|
|
19
|
-
const isConditional = !!conditionalMatch;
|
|
20
|
-
return {
|
|
21
|
-
line: lineNumber,
|
|
22
|
-
text: stepText,
|
|
23
|
-
optional: isOptional,
|
|
24
|
-
conditional: isConditional,
|
|
25
|
-
condition: conditionalMatch ? conditionalMatch[1] : undefined,
|
|
26
|
-
action: conditionalMatch ? conditionalMatch[2] : undefined,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Extract comments from a flow file
|
|
31
|
-
*/
|
|
32
|
-
function extractComments(content) {
|
|
33
|
-
return content
|
|
34
|
-
.split("\n")
|
|
35
|
-
.filter((line) => line.trim().startsWith("#"))
|
|
36
|
-
.map((line) => line.trim().substring(1).trim());
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Parse a .flow file into a FlowFile object
|
|
40
|
-
*/
|
|
41
|
-
export function parseFlowFile(filePath) {
|
|
42
|
-
if (!fs.existsSync(filePath)) {
|
|
43
|
-
throw new Error(`Flow file not found: ${filePath}`);
|
|
44
|
-
}
|
|
45
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
46
|
-
const lines = content.split("\n");
|
|
47
|
-
const steps = [];
|
|
48
|
-
for (let i = 0; i < lines.length; i++) {
|
|
49
|
-
const step = parseLine(lines[i], i + 1);
|
|
50
|
-
if (step) {
|
|
51
|
-
steps.push(step);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
path: filePath,
|
|
56
|
-
name: path.basename(filePath, ".flow"),
|
|
57
|
-
steps,
|
|
58
|
-
comments: extractComments(content),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Parse flow content from a string (for inline tests)
|
|
63
|
-
*/
|
|
64
|
-
export function parseFlowContent(content, name = "inline") {
|
|
65
|
-
const lines = content.split("\n");
|
|
66
|
-
const steps = [];
|
|
67
|
-
for (let i = 0; i < lines.length; i++) {
|
|
68
|
-
const step = parseLine(lines[i], i + 1);
|
|
69
|
-
if (step) {
|
|
70
|
-
steps.push(step);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
path: "",
|
|
75
|
-
name,
|
|
76
|
-
steps,
|
|
77
|
-
comments: extractComments(content),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Find all .flow files in a directory
|
|
82
|
-
*/
|
|
83
|
-
export async function findFlowFiles(dir) {
|
|
84
|
-
const pattern = path.join(dir, "**/*.flow");
|
|
85
|
-
return glob(pattern, { nodir: true });
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Validate a flow file for common issues
|
|
89
|
-
*/
|
|
90
|
-
export function validateFlowFile(flow) {
|
|
91
|
-
const warnings = [];
|
|
92
|
-
if (flow.steps.length === 0) {
|
|
93
|
-
warnings.push("Flow file has no steps");
|
|
94
|
-
}
|
|
95
|
-
// Check for steps that might be incomplete
|
|
96
|
-
for (const step of flow.steps) {
|
|
97
|
-
if (step.text.length < 3) {
|
|
98
|
-
warnings.push(`Line ${step.line}: Step seems too short: "${step.text}"`);
|
|
99
|
-
}
|
|
100
|
-
if (step.conditional && !step.action) {
|
|
101
|
-
warnings.push(`Line ${step.line}: Conditional step missing action`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return warnings;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Get a summary of a flow file
|
|
108
|
-
*/
|
|
109
|
-
export function getFlowSummary(flow) {
|
|
110
|
-
return {
|
|
111
|
-
totalSteps: flow.steps.length,
|
|
112
|
-
requiredSteps: flow.steps.filter((s) => !s.optional).length,
|
|
113
|
-
optionalSteps: flow.steps.filter((s) => s.optional).length,
|
|
114
|
-
conditionalSteps: flow.steps.filter((s) => s.conditional).length,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
//# sourceMappingURL=flow.js.map
|
|
1
|
+
import t from"fs";import n from"path";import{glob as o}from"glob";function e(t,n){const o=t.trim();if(!o||o.startsWith("#"))return null;const e=o.match(/^\[Optional\]\s*(.+)$/i),i=!!e,s=e?e[1]:o,r=s.match(/^If\s+(.+?)\s*,\s*(.+)$/i);return{line:n,text:s,optional:i,conditional:!!r,condition:r?r[1]:void 0,action:r?r[2]:void 0}}function i(t){return t.split("\n").filter(t=>t.trim().startsWith("#")).map(t=>t.trim().substring(1).trim())}export function parseFlowFile(o){if(!t.existsSync(o))throw new Error(`Flow file not found: ${o}`);const s=t.readFileSync(o,"utf-8"),r=s.split("\n"),l=[];for(let t=0;t<r.length;t++){const n=e(r[t],t+1);n&&l.push(n)}return{path:o,name:n.basename(o,".flow"),steps:l,comments:i(s)}}export function parseFlowContent(t,n="inline"){const o=t.split("\n"),s=[];for(let t=0;t<o.length;t++){const n=e(o[t],t+1);n&&s.push(n)}return{path:"",name:n,steps:s,comments:i(t)}}export async function findFlowFiles(t){const e=n.join(t,"**/*.flow");return o(e,{nodir:!0})}export function validateFlowFile(t){const n=[];0===t.steps.length&&n.push("Flow file has no steps");for(const o of t.steps)o.text.length<3&&n.push(`Line ${o.line}: Step seems too short: "${o.text}"`),o.conditional&&!o.action&&n.push(`Line ${o.line}: Conditional step missing action`);return n}export function getFlowSummary(t){return{totalSteps:t.steps.length,requiredSteps:t.steps.filter(t=>!t.optional).length,optionalSteps:t.steps.filter(t=>t.optional).length,conditionalSteps:t.steps.filter(t=>t.conditional).length}}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance auditing module — four independent layers:
|
|
3
|
+
* 1. Core Web Vitals — injected into running browser, zero extra deps
|
|
4
|
+
* 2. Network analysis — resource timing + fetch/XHR interception + long tasks
|
|
5
|
+
* 3. Framework — React/Next.js detection + re-render analysis with interactions
|
|
6
|
+
* 4. Deep audit — isolated headless Chrome for cold-start throttled scores
|
|
7
|
+
*/
|
|
8
|
+
import { BrowserAgent } from "../browser/agent.js";
|
|
9
|
+
export interface WebVitals {
|
|
10
|
+
/** First Contentful Paint (ms) */
|
|
11
|
+
fcp?: number;
|
|
12
|
+
/** Largest Contentful Paint (ms) */
|
|
13
|
+
lcp?: number;
|
|
14
|
+
/** Cumulative Layout Shift */
|
|
15
|
+
cls?: number;
|
|
16
|
+
/** Time to First Byte (ms) */
|
|
17
|
+
ttfb?: number;
|
|
18
|
+
/** DOM Content Loaded (ms) */
|
|
19
|
+
domContentLoaded?: number;
|
|
20
|
+
/** Page fully loaded (ms) */
|
|
21
|
+
loadComplete?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface LighthouseScores {
|
|
24
|
+
performance: number;
|
|
25
|
+
accessibility: number;
|
|
26
|
+
bestPractices: number;
|
|
27
|
+
seo: number;
|
|
28
|
+
fcp?: number;
|
|
29
|
+
lcp?: number;
|
|
30
|
+
cls?: number;
|
|
31
|
+
/** Total Blocking Time (ms) */
|
|
32
|
+
tbt?: number;
|
|
33
|
+
/** Speed Index */
|
|
34
|
+
speedIndex?: number;
|
|
35
|
+
/** Time to Interactive (ms) */
|
|
36
|
+
tti?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ReactScanResult {
|
|
39
|
+
detected: boolean;
|
|
40
|
+
version?: string;
|
|
41
|
+
/** Components with static-passive re-render issues (from react-scan) */
|
|
42
|
+
issues: Array<{
|
|
43
|
+
component: string;
|
|
44
|
+
renderCount: number;
|
|
45
|
+
avgMs?: number;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Results of simulated user interactions — each entry describes what was
|
|
49
|
+
* clicked, how many DOM mutations it triggered, and whether that count is
|
|
50
|
+
* suspiciously high.
|
|
51
|
+
*/
|
|
52
|
+
interactionTests?: Array<{
|
|
53
|
+
action: string;
|
|
54
|
+
mutations: number;
|
|
55
|
+
flagged: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
/** A single network request captured via fetch/XHR interception */
|
|
59
|
+
export interface NetworkRequest {
|
|
60
|
+
url: string;
|
|
61
|
+
method: string;
|
|
62
|
+
status: number;
|
|
63
|
+
/** Response time in ms */
|
|
64
|
+
duration: number;
|
|
65
|
+
type: "fetch" | "xhr";
|
|
66
|
+
/** true for 4xx/5xx or network error */
|
|
67
|
+
failed?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** A resource loaded by the browser (script, stylesheet, image, font…) */
|
|
70
|
+
export interface ResourceEntry {
|
|
71
|
+
url: string;
|
|
72
|
+
type: string;
|
|
73
|
+
/** Transfer size in bytes (0 if served from cache) */
|
|
74
|
+
size: number;
|
|
75
|
+
/** Uncompressed body size in bytes */
|
|
76
|
+
encodedSize: number;
|
|
77
|
+
/** Load duration in ms */
|
|
78
|
+
duration: number;
|
|
79
|
+
/** true if this resource blocked first render */
|
|
80
|
+
renderBlocking?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/** A JavaScript task that blocked the main thread for >50ms */
|
|
83
|
+
export interface LongTask {
|
|
84
|
+
/** Duration in ms */
|
|
85
|
+
duration: number;
|
|
86
|
+
startTime: number;
|
|
87
|
+
}
|
|
88
|
+
export interface NetworkAnalysis {
|
|
89
|
+
/** Total number of resources loaded */
|
|
90
|
+
totalRequests: number;
|
|
91
|
+
/** Uncompressed bytes transferred (resources only) */
|
|
92
|
+
totalBytes: number;
|
|
93
|
+
/** Bytes from JS files */
|
|
94
|
+
jsBytes: number;
|
|
95
|
+
/** Bytes from CSS files */
|
|
96
|
+
cssBytes: number;
|
|
97
|
+
/** Bytes from images */
|
|
98
|
+
imageBytes: number;
|
|
99
|
+
/** Resources that blocked rendering */
|
|
100
|
+
renderBlockingCount: number;
|
|
101
|
+
/** Top 10 heaviest resources */
|
|
102
|
+
heaviestResources: ResourceEntry[];
|
|
103
|
+
/** API calls captured during the session (fetch + XHR) */
|
|
104
|
+
apiCalls: NetworkRequest[];
|
|
105
|
+
/** API calls slower than 500ms */
|
|
106
|
+
slowApiCalls: NetworkRequest[];
|
|
107
|
+
/** API calls that returned an error status */
|
|
108
|
+
failedApiCalls: NetworkRequest[];
|
|
109
|
+
/** Long tasks (JS blocking main thread >50ms) */
|
|
110
|
+
longTasks: LongTask[];
|
|
111
|
+
/** Total main-thread blocking time from long tasks (ms) */
|
|
112
|
+
totalBlockingMs: number;
|
|
113
|
+
/** JS heap size in MB, if available */
|
|
114
|
+
memoryMB?: number;
|
|
115
|
+
}
|
|
116
|
+
export interface PerfAuditResult {
|
|
117
|
+
url: string;
|
|
118
|
+
auditedAt: string;
|
|
119
|
+
/** Always collected from the live browser session */
|
|
120
|
+
vitals: WebVitals;
|
|
121
|
+
/** null when the deep audit is unavailable or disabled */
|
|
122
|
+
scores: LighthouseScores | null;
|
|
123
|
+
/** null when framework not detected or analysis disabled */
|
|
124
|
+
react: ReactScanResult | null;
|
|
125
|
+
/** Network analysis — resource sizes, API calls, long tasks */
|
|
126
|
+
network?: NetworkAnalysis;
|
|
127
|
+
/** @deprecated use scores */
|
|
128
|
+
lighthouse?: LighthouseScores | null;
|
|
129
|
+
/** Path to full audit HTML report, if saved */
|
|
130
|
+
lighthouseReportPath?: string;
|
|
131
|
+
}
|
|
132
|
+
export interface PerfAuditOptions {
|
|
133
|
+
/** Run deep performance audit (default: true) */
|
|
134
|
+
lighthouse?: boolean;
|
|
135
|
+
/** Analyse framework re-renders including interaction tests (default: true) */
|
|
136
|
+
reactScan?: boolean;
|
|
137
|
+
/** Wait this many ms after page load before collecting vitals (default: 2000) */
|
|
138
|
+
settleMs?: number;
|
|
139
|
+
/** Save full audit HTML report to this directory */
|
|
140
|
+
lighthouseOutputDir?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Navigate to `url` before auditing (default: true for task agent).
|
|
143
|
+
* Set to false when the browser is already on the target page (flow runner).
|
|
144
|
+
*/
|
|
145
|
+
navigate?: boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Injects a PerformanceObserver into the current page that collects
|
|
149
|
+
* FCP, LCP, CLS and also reads Navigation Timing for TTFB / DOM ready.
|
|
150
|
+
* Call collectCoreWebVitals() after some settle time to harvest results.
|
|
151
|
+
*/
|
|
152
|
+
export declare function injectVitalsObserver(browser: BrowserAgent): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Reads vitals from the injected observer + Navigation Timing API.
|
|
155
|
+
*/
|
|
156
|
+
export declare function collectCoreWebVitals(browser: BrowserAgent): Promise<WebVitals>;
|
|
157
|
+
/**
|
|
158
|
+
* Injects fetch/XHR interceptors and a LongTask PerformanceObserver into the
|
|
159
|
+
* page. Must be called BEFORE user interactions or lazy-loading happens so that
|
|
160
|
+
* API calls during the session are captured.
|
|
161
|
+
*
|
|
162
|
+
* Already-completed load-time requests are captured separately via
|
|
163
|
+
* performance.getEntriesByType('resource') in collectNetworkAnalysis().
|
|
164
|
+
*/
|
|
165
|
+
export declare function injectNetworkTrackers(browser: BrowserAgent): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Collects all network data from the page:
|
|
168
|
+
* - Resource timing (all loaded files — JS, CSS, images, fonts)
|
|
169
|
+
* - Captured fetch/XHR API calls (from injectNetworkTrackers)
|
|
170
|
+
* - Long tasks
|
|
171
|
+
* - JS heap memory if available
|
|
172
|
+
*/
|
|
173
|
+
export declare function collectNetworkAnalysis(browser: BrowserAgent): Promise<NetworkAnalysis | null>;
|
|
174
|
+
/**
|
|
175
|
+
* Injects React Scan from unpkg into the current page.
|
|
176
|
+
* Uses comprehensive detection so Next.js / production builds are caught.
|
|
177
|
+
* Does NOT cache detection state — always re-detects to avoid stale negatives.
|
|
178
|
+
*/
|
|
179
|
+
export declare function injectReactScan(browser: BrowserAgent): Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Collects framework detection and re-render analysis results.
|
|
182
|
+
* Always runs fresh detection — never relies on a cached window.__reactDetection
|
|
183
|
+
* that might have been set to {detected:false} by an older code path.
|
|
184
|
+
*/
|
|
185
|
+
export declare function collectReactScanResults(browser: BrowserAgent): Promise<ReactScanResult | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Runs a full Lighthouse audit against a URL.
|
|
188
|
+
*
|
|
189
|
+
* Lighthouse ALWAYS needs its own fresh Chrome for accurate scores — it applies
|
|
190
|
+
* controlled CPU/network throttling and does multiple cold-start page loads.
|
|
191
|
+
* Reusing the agent-browser session would skew every metric.
|
|
192
|
+
*
|
|
193
|
+
* chrome-launcher is a transitive dependency of lighthouse — we don't need to
|
|
194
|
+
* import it directly. Lighthouse manages Chrome internally.
|
|
195
|
+
*
|
|
196
|
+
* Returns null if lighthouse is unavailable or the audit fails.
|
|
197
|
+
*/
|
|
198
|
+
export declare function runLighthouseAudit(url: string, outputDir?: string): Promise<{
|
|
199
|
+
scores: LighthouseScores;
|
|
200
|
+
reportPath?: string;
|
|
201
|
+
} | null>;
|
|
202
|
+
/**
|
|
203
|
+
* Run a full performance audit while a browser session is already open.
|
|
204
|
+
*
|
|
205
|
+
* CWV + framework analysis are collected from the live browser session.
|
|
206
|
+
* The deep performance audit spins up its own fresh headless Chrome for accurate
|
|
207
|
+
* throttled scores — this is intentional: it needs controlled cold-start
|
|
208
|
+
* conditions that the existing session cannot provide.
|
|
209
|
+
*
|
|
210
|
+
* In the flow runner we avoid the overlap by closing agent-browser first (see
|
|
211
|
+
* runner/index.ts). In the task agent the overlap is brief and acceptable since
|
|
212
|
+
* the user explicitly asked for a perf audit mid-session.
|
|
213
|
+
*/
|
|
214
|
+
export declare function runPerfAudit(url: string, browser: BrowserAgent, options?: PerfAuditOptions): Promise<PerfAuditResult>;
|
|
215
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function injectVitalsObserver(e){try{await e.evaluate("(function(){\n if(window.__slapifyVitals) return;\n window.__slapifyVitals = { fcp: null, lcp: null, cls: 0, observed: false };\n try {\n var fcpObs = new PerformanceObserver(function(list){\n var e = list.getEntriesByName('first-contentful-paint')[0];\n if(e) window.__slapifyVitals.fcp = Math.round(e.startTime);\n });\n fcpObs.observe({ type: 'paint', buffered: true });\n\n var lcpObs = new PerformanceObserver(function(list){\n var entries = list.getEntries();\n if(entries.length) window.__slapifyVitals.lcp = Math.round(entries[entries.length-1].startTime);\n });\n lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });\n\n var clsObs = new PerformanceObserver(function(list){\n list.getEntries().forEach(function(e){\n if(!e.hadRecentInput) window.__slapifyVitals.cls += e.value;\n });\n });\n clsObs.observe({ type: 'layout-shift', buffered: true });\n\n window.__slapifyVitals.observed = true;\n } catch(err) {}\n })()")}catch{}}export async function collectCoreWebVitals(e){try{const t=await e.evaluate("(function(){\n var v = window.__slapifyVitals || {};\n var nav = performance.getEntriesByType('navigation')[0] || {};\n return JSON.stringify({\n fcp: v.fcp || null,\n lcp: v.lcp || null,\n cls: v.cls != null ? +v.cls.toFixed(4) : null,\n ttfb: nav.responseStart ? Math.round(nav.responseStart) : null,\n domContentLoaded: nav.domContentLoadedEventEnd ? Math.round(nav.domContentLoadedEventEnd) : null,\n loadComplete: nav.loadEventEnd ? Math.round(nav.loadEventEnd) : null\n });\n })()"),n=t.startsWith('"')?JSON.parse(t):t,r=JSON.parse(n),a={};return null!=r.fcp&&(a.fcp=r.fcp),null!=r.lcp&&(a.lcp=r.lcp),null!=r.cls&&(a.cls=r.cls),null!=r.ttfb&&(a.ttfb=r.ttfb),null!=r.domContentLoaded&&(a.domContentLoaded=r.domContentLoaded),null!=r.loadComplete&&(a.loadComplete=r.loadComplete),a}catch{return{}}}export async function injectNetworkTrackers(e){try{await e.evaluate("(function(){\n if (window.__slapifyNet) return;\n window.__slapifyNet = { requests: [], longTasks: [] };\n\n // ── fetch interceptor ──────────────────────────────────────────────────\n var _fetch = window.fetch;\n window.fetch = function(input, init) {\n var url = '';\n if (typeof input === 'string') url = input;\n else if (input instanceof URL) url = input.href;\n else if (input && input.url) url = input.url;\n url = url.slice(0, 300);\n var method = (init && init.method) || (input && input.method) || 'GET';\n var t0 = performance.now();\n return _fetch.apply(this, arguments)\n .then(function(r) {\n window.__slapifyNet.requests.push({ url: url, method: method.toUpperCase(), status: r.status, duration: Math.round(performance.now()-t0), type: 'fetch', failed: r.status >= 400 });\n return r;\n })\n .catch(function(e) {\n window.__slapifyNet.requests.push({ url: url, method: method.toUpperCase(), status: 0, duration: Math.round(performance.now()-t0), type: 'fetch', failed: true });\n throw e;\n });\n };\n\n // ── XHR interceptor ───────────────────────────────────────────────────\n var _open = XMLHttpRequest.prototype.open;\n var _send = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function(m, u) {\n this.__slapM = m; this.__slapU = (u||'').toString().slice(0,300);\n return _open.apply(this, arguments);\n };\n XMLHttpRequest.prototype.send = function() {\n var self = this, t0 = performance.now();\n this.addEventListener('loadend', function() {\n window.__slapifyNet.requests.push({ url: self.__slapU||'', method: (self.__slapM||'GET').toUpperCase(), status: self.status, duration: Math.round(performance.now()-t0), type: 'xhr', failed: self.status >= 400 || self.status === 0 });\n });\n return _send.apply(this, arguments);\n };\n\n // ── Long task observer ────────────────────────────────────────────────\n try {\n var obs = new PerformanceObserver(function(list) {\n list.getEntries().forEach(function(e) {\n window.__slapifyNet.longTasks.push({ duration: Math.round(e.duration), startTime: Math.round(e.startTime) });\n });\n });\n obs.observe({ type: 'longtask', buffered: true });\n } catch(e) {}\n })()")}catch{}}export async function collectNetworkAnalysis(e){try{const t=await e.evaluate("(function(){\n // Resource timing — all loaded files\n var resources = [];\n try {\n performance.getEntriesByType('resource').forEach(function(e) {\n resources.push({\n url: e.name.slice(0, 300),\n type: e.initiatorType || 'other',\n size: e.transferSize || 0,\n encodedSize: e.encodedBodySize || 0,\n duration: Math.round(e.duration),\n renderBlocking: e.renderBlockingStatus === 'blocking'\n });\n });\n } catch(e) {}\n\n var net = window.__slapifyNet || { requests: [], longTasks: [] };\n\n // Memory\n var memMB = null;\n try { if (performance.memory) memMB = +(performance.memory.usedJSHeapSize / 1048576).toFixed(1); } catch(e){}\n\n return JSON.stringify({ resources: resources, requests: net.requests, longTasks: net.longTasks, memMB: memMB });\n })()"),n=t.startsWith('"')?JSON.parse(t):t,r=JSON.parse(n),a=r.resources||[],i=r.requests||[],s=r.longTasks||[];let o=0,l=0,c=0,u=0,d=0;for(const e of a)o+=e.size||0,"script"===e.type?l+=e.size||0:"css"===e.type||"link"===e.type?c+=e.size||0:"img"!==e.type&&"image"!==e.type||(u+=e.size||0),e.renderBlocking&&d++;const f=[...a].sort((e,t)=>(t.size||0)-(e.size||0)).slice(0,10),p=i.filter(e=>e.duration>=500),w=i.filter(e=>e.failed),h=s.reduce((e,t)=>e+t.duration,0),m={totalRequests:a.length,totalBytes:o,jsBytes:l,cssBytes:c,imageBytes:u,renderBlockingCount:d,heaviestResources:f,apiCalls:i,slowApiCalls:p,failedApiCalls:w,longTasks:s,totalBlockingMs:h};return null!=r.memMB&&(m.memoryMB=r.memMB),m}catch{return null}}const e="(function(){\n // 1. React fiber on DOM nodes — works for ALL production React builds including Next.js App Router.\n // Scan a broad set of elements; fibers are attached to every rendered node.\n var els = document.querySelectorAll('*');\n var limit = Math.min(els.length, 200);\n for(var i = 0; i < limit; i++) {\n var el = els[i];\n // Only check elements that likely have React attached (skip pure HTML/SVG with no properties)\n var keys;\n try { keys = Object.keys(el); } catch(e) { continue; }\n for(var j = 0; j < keys.length; j++) {\n var k = keys[j];\n if(k.startsWith('__reactFiber') || k.startsWith('__reactProps') || k.startsWith('__reactInternalInstance') || k.startsWith('__reactEventHandlers')) {\n // Detect Next.js via global markers while we're at it\n var framework = (window.__NEXT_DATA__ || window.__next_f || window.next) ? 'Next.js' : 'React';\n return JSON.stringify({ detected: true, framework: framework, version: null });\n }\n }\n }\n\n // 2. Next.js globals — Pages Router (__NEXT_DATA__), App Router (__next_f streaming chunks)\n if(window.__NEXT_DATA__) return JSON.stringify({ detected: true, framework: 'Next.js (Pages)', version: null });\n if(window.__next_f || (Array.isArray(window.nd) && window.nd.length)) return JSON.stringify({ detected: true, framework: 'Next.js (App Router)', version: null });\n\n // 3. _next/static script tags — present on every Next.js deployment\n var scripts = document.querySelectorAll('script[src*=\"/_next/static\"]');\n if(scripts.length > 0) return JSON.stringify({ detected: true, framework: 'Next.js', version: null });\n\n // 4. DevTools hook with active renderers (dev mode or React DevTools extension)\n var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n if(hook && hook.renderers && hook.renderers.size > 0)\n return JSON.stringify({ detected: true, framework: 'React', version: null });\n\n // 5. window.React (dev builds / CRA / explicit exposure)\n if(window.React) return JSON.stringify({ detected: true, framework: 'React', version: window.React.version || null });\n\n // 6. Legacy attributes\n if(document.querySelector('[data-reactroot],[data-reactid]'))\n return JSON.stringify({ detected: true, framework: 'React (legacy)', version: null });\n\n return JSON.stringify({ detected: false, framework: null, version: null });\n})()";export async function injectReactScan(t){const n=`(function(){\n // Always re-detect; do not skip based on prior state — prior detection\n // might have been a false negative from an old browser session.\n var raw = ${e};\n var info = JSON.parse(typeof raw === 'string' ? raw : '{"detected":false}');\n window.__reactDetection = info;\n if(!info.detected) return;\n\n // Only inject react-scan once per page load\n if(window.__reactScanInjected) return;\n window.__reactScanInjected = true;\n\n var s = document.createElement('script');\n s.src = 'https://unpkg.com/react-scan/dist/auto.global.js';\n s.onload = function(){\n try {\n if(window.reactScan) window.reactScan.setOptions({ enabled: true, showToolbar: false });\n } catch(e) {}\n };\n document.head.appendChild(s);\n })()`;try{await t.evaluate(n)}catch{}}export async function collectReactScanResults(t){const n=`(function(){\n var raw = ${e};\n var detection = JSON.parse(typeof raw === 'string' ? raw : '{"detected":false}');\n return JSON.stringify({\n isReact: detection.detected,\n framework: detection.framework || null,\n reactVersion: detection.version || (window.React && window.React.version) || null,\n scanAvailable: !!(window.reactScan && window.reactScan.getReport)\n });\n })()`;try{const e=await t.evaluate(n),r=e.startsWith('"')?JSON.parse(e):e,a=JSON.parse(r);if(!a.isReact)return{detected:!1,issues:[]};const i={detected:!0,version:a.reactVersion||(a.framework?`(${a.framework})`:void 0),issues:[]};if(a.scanAvailable){const e="(function(){\n try {\n var report = window.reactScan.getReport();\n if(!report) return 'null';\n var issues = [];\n report.forEach(function(v, k){\n if(v.count > 2) {\n issues.push({ component: k, renderCount: v.count, avgMs: v.time ? +(v.time/v.count).toFixed(1) : null });\n }\n });\n issues.sort(function(a,b){ return b.renderCount - a.renderCount; });\n return JSON.stringify(issues.slice(0, 20));\n } catch(e) { return '[]'; }\n })()";try{const n=await t.evaluate(e),r=n.startsWith('"')?JSON.parse(n):n;i.issues=JSON.parse(r)||[]}catch{}}const s=await async function(e){const t=[];let n=[];try{const t=await e.evaluate("(function(){\n window.__slapifyMutCount = 0;\n if (window.__slapifyMutObs) { try { window.__slapifyMutObs.disconnect(); } catch(e){} }\n window.__slapifyMutObs = new MutationObserver(function(records) {\n records.forEach(function(r) {\n window.__slapifyMutCount += r.addedNodes.length + r.removedNodes.length + (r.type === 'attributes' ? 1 : 0);\n });\n });\n window.__slapifyMutObs.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: false });\n\n var seen = {};\n var found = [];\n var selectors = 'button:not([type=\"submit\"]):not([disabled]), [role=\"tab\"], [role=\"button\"]:not(a), [aria-expanded]';\n document.querySelectorAll(selectors).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (el.offsetParent !== null && rect.width > 20 && rect.height > 10) {\n var label = ((el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '')).replace(/\\s+/g, ' ').trim().slice(0, 40);\n if (label && !seen[label]) {\n seen[label] = true;\n found.push(label);\n }\n }\n });\n window.__slapifyInteractables = found.slice(0, 6);\n return JSON.stringify(window.__slapifyInteractables);\n })()"),r=t.startsWith('"')?JSON.parse(t):t;n=JSON.parse(r)||[]}catch{return t}for(let r=0;r<n.length;r++)try{if(await e.evaluate("(function(){ window.__slapifyMutCount = 0; return 'ok'; })()"),!(await e.evaluate(`(function(){\n var label = window.__slapifyInteractables[${r}];\n var selectors = 'button:not([type="submit"]):not([disabled]), [role="tab"], [role="button"]:not(a), [aria-expanded]';\n var target = Array.from(document.querySelectorAll(selectors)).find(function(el) {\n var t = ((el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '')).replace(/\\s+/g, ' ').trim().slice(0, 40);\n return t === label;\n });\n if (!target) return 'not_found';\n try { target.click(); return 'clicked'; } catch(e) { return 'error'; }\n })()`)).includes("clicked"))continue;await new Promise(e=>setTimeout(e,800));const a=await e.evaluate("(function(){ return String(window.__slapifyMutCount || 0); })()"),i=parseInt(a.replace(/\D/g,""))||0;t.push({action:`Clicked: ${n[r]}`,mutations:i,flagged:i>50})}catch{}try{await e.evaluate("(function(){ if(window.__slapifyMutObs){ window.__slapifyMutObs.disconnect(); window.__slapifyMutObs=null; } return 'ok'; })()")}catch{}return t}(t);return(s??[]).length>0&&(i.interactionTests=s),i}catch{return null}}export async function runLighthouseAudit(e,t){try{const{default:n}=await import("lighthouse"),{launch:r}=await import("chrome-launcher"),a=await r({chromeFlags:["--headless=new","--no-sandbox","--disable-gpu"]});try{const r=await n(e,{port:a.port,output:t?["json","html"]:"json",logLevel:"error",onlyCategories:["performance","accessibility","best-practices","seo"],skipAudits:["screenshot-thumbnails","final-screenshot"]});if(!r?.lhr)return null;const i=r.lhr,s=i.categories??{},o=i.audits??{},l={performance:Math.round(100*(s.performance?.score??0)),accessibility:Math.round(100*(s.accessibility?.score??0)),bestPractices:Math.round(100*(s["best-practices"]?.score??0)),seo:Math.round(100*(s.seo?.score??0)),fcp:null!=o["first-contentful-paint"]?.numericValue?Math.round(o["first-contentful-paint"].numericValue):void 0,lcp:null!=o["largest-contentful-paint"]?.numericValue?Math.round(o["largest-contentful-paint"].numericValue):void 0,cls:null!=o["cumulative-layout-shift"]?.numericValue?+o["cumulative-layout-shift"].numericValue.toFixed(4):void 0,tbt:null!=o["total-blocking-time"]?.numericValue?Math.round(o["total-blocking-time"].numericValue):void 0,speedIndex:null!=o["speed-index"]?.numericValue?Math.round(o["speed-index"].numericValue):void 0,tti:null!=o.interactive?.numericValue?Math.round(o.interactive.numericValue):void 0};let c;if(t&&r.report){const{default:n}=await import("fs"),{default:a}=await import("path");n.mkdirSync(t,{recursive:!0});const i=(Array.isArray(r.report)?r.report:[r.report]).find(e=>e.startsWith("<!"));if(i){const r=new URL(e).hostname.replace(/\./g,"-");c=a.join(t,`lighthouse-${r}-${Date.now()}.html`),n.writeFileSync(c,i)}}return{scores:l,reportPath:c}}finally{await a.kill()}}catch{return null}}export async function runPerfAudit(e,t,n={}){const{lighthouse:r=!0,reactScan:a=!0,settleMs:i=2e3,lighthouseOutputDir:s,navigate:o=!0}=n;o&&(await t.navigate(e),await new Promise(e=>setTimeout(e,500)));const l={url:e,auditedAt:(new Date).toISOString(),vitals:{},scores:null,react:null};if(await injectVitalsObserver(t),await injectNetworkTrackers(t),a&&await injectReactScan(t),await new Promise(e=>setTimeout(e,i)),l.vitals=await collectCoreWebVitals(t),a&&(l.react=await collectReactScanResults(t)),l.network=await collectNetworkAnalysis(t)??void 0,r){const t=await runLighthouseAudit(e,s);t&&(l.scores=t.scores,l.lighthouse=t.scores,t.reportPath&&(l.lighthouseReportPath=t.reportPath))}return l}
|