@shadowcoderr/context-graph 0.3.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/dist/analyzers/a11y-extractor.d.ts +15 -0
  4. package/dist/analyzers/a11y-extractor.d.ts.map +1 -0
  5. package/dist/analyzers/a11y-extractor.js +148 -0
  6. package/dist/analyzers/a11y-extractor.js.map +1 -0
  7. package/dist/analyzers/dom-analyzer.d.ts +20 -0
  8. package/dist/analyzers/dom-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/dom-analyzer.js +126 -0
  10. package/dist/analyzers/dom-analyzer.js.map +1 -0
  11. package/dist/analyzers/locator-generator.d.ts +13 -0
  12. package/dist/analyzers/locator-generator.d.ts.map +1 -0
  13. package/dist/analyzers/locator-generator.js +381 -0
  14. package/dist/analyzers/locator-generator.js.map +1 -0
  15. package/dist/analyzers/network-logger.d.ts +15 -0
  16. package/dist/analyzers/network-logger.d.ts.map +1 -0
  17. package/dist/analyzers/network-logger.js +71 -0
  18. package/dist/analyzers/network-logger.js.map +1 -0
  19. package/dist/cli/index.d.ts +2 -0
  20. package/dist/cli/index.d.ts.map +1 -0
  21. package/dist/cli/index.js +155 -0
  22. package/dist/cli/index.js.map +1 -0
  23. package/dist/config/defaults.d.ts +3 -0
  24. package/dist/config/defaults.d.ts.map +1 -0
  25. package/dist/config/defaults.js +54 -0
  26. package/dist/config/defaults.js.map +1 -0
  27. package/dist/config/loader.d.ts +3 -0
  28. package/dist/config/loader.d.ts.map +1 -0
  29. package/dist/config/loader.js +75 -0
  30. package/dist/config/loader.js.map +1 -0
  31. package/dist/config/schema.d.ts +168 -0
  32. package/dist/config/schema.d.ts.map +1 -0
  33. package/dist/config/schema.js +104 -0
  34. package/dist/config/schema.js.map +1 -0
  35. package/dist/core/browser-adapter.d.ts +24 -0
  36. package/dist/core/browser-adapter.d.ts.map +1 -0
  37. package/dist/core/browser-adapter.js +208 -0
  38. package/dist/core/browser-adapter.js.map +1 -0
  39. package/dist/core/capture-engine.d.ts +52 -0
  40. package/dist/core/capture-engine.d.ts.map +1 -0
  41. package/dist/core/capture-engine.js +593 -0
  42. package/dist/core/capture-engine.js.map +1 -0
  43. package/dist/core/runtime.d.ts +38 -0
  44. package/dist/core/runtime.d.ts.map +1 -0
  45. package/dist/core/runtime.js +648 -0
  46. package/dist/core/runtime.js.map +1 -0
  47. package/dist/prompts/init-prompt.d.ts +12 -0
  48. package/dist/prompts/init-prompt.d.ts.map +1 -0
  49. package/dist/prompts/init-prompt.js +128 -0
  50. package/dist/prompts/init-prompt.js.map +1 -0
  51. package/dist/registry/components-registry.d.ts +97 -0
  52. package/dist/registry/components-registry.d.ts.map +1 -0
  53. package/dist/registry/components-registry.js +469 -0
  54. package/dist/registry/components-registry.js.map +1 -0
  55. package/dist/registry/index.d.ts +2 -0
  56. package/dist/registry/index.d.ts.map +1 -0
  57. package/dist/registry/index.js +7 -0
  58. package/dist/registry/index.js.map +1 -0
  59. package/dist/security/patterns.d.ts +4 -0
  60. package/dist/security/patterns.d.ts.map +1 -0
  61. package/dist/security/patterns.js +65 -0
  62. package/dist/security/patterns.js.map +1 -0
  63. package/dist/security/redactor.d.ts +26 -0
  64. package/dist/security/redactor.d.ts.map +1 -0
  65. package/dist/security/redactor.js +128 -0
  66. package/dist/security/redactor.js.map +1 -0
  67. package/dist/security/validator.d.ts +11 -0
  68. package/dist/security/validator.d.ts.map +1 -0
  69. package/dist/security/validator.js +62 -0
  70. package/dist/security/validator.js.map +1 -0
  71. package/dist/storage/engine.d.ts +45 -0
  72. package/dist/storage/engine.d.ts.map +1 -0
  73. package/dist/storage/engine.js +479 -0
  74. package/dist/storage/engine.js.map +1 -0
  75. package/dist/storage/manifest.d.ts +10 -0
  76. package/dist/storage/manifest.d.ts.map +1 -0
  77. package/dist/storage/manifest.js +98 -0
  78. package/dist/storage/manifest.js.map +1 -0
  79. package/dist/storage/serializer.d.ts +9 -0
  80. package/dist/storage/serializer.d.ts.map +1 -0
  81. package/dist/storage/serializer.js +22 -0
  82. package/dist/storage/serializer.js.map +1 -0
  83. package/dist/types/capture.d.ts +206 -0
  84. package/dist/types/capture.d.ts.map +1 -0
  85. package/dist/types/capture.js +3 -0
  86. package/dist/types/capture.js.map +1 -0
  87. package/dist/types/config.d.ts +63 -0
  88. package/dist/types/config.d.ts.map +1 -0
  89. package/dist/types/config.js +3 -0
  90. package/dist/types/config.js.map +1 -0
  91. package/dist/types/registry.d.ts +94 -0
  92. package/dist/types/registry.d.ts.map +1 -0
  93. package/dist/types/registry.js +3 -0
  94. package/dist/types/registry.js.map +1 -0
  95. package/dist/types/storage.d.ts +57 -0
  96. package/dist/types/storage.d.ts.map +1 -0
  97. package/dist/types/storage.js +3 -0
  98. package/dist/types/storage.js.map +1 -0
  99. package/dist/utils/hash.d.ts +3 -0
  100. package/dist/utils/hash.d.ts.map +1 -0
  101. package/dist/utils/hash.js +26 -0
  102. package/dist/utils/hash.js.map +1 -0
  103. package/dist/utils/logger.d.ts +20 -0
  104. package/dist/utils/logger.d.ts.map +1 -0
  105. package/dist/utils/logger.js +86 -0
  106. package/dist/utils/logger.js.map +1 -0
  107. package/dist/utils/pom-generator.d.ts +12 -0
  108. package/dist/utils/pom-generator.d.ts.map +1 -0
  109. package/dist/utils/pom-generator.js +83 -0
  110. package/dist/utils/pom-generator.js.map +1 -0
  111. package/dist/utils/validators.d.ts +7 -0
  112. package/dist/utils/validators.d.ts.map +1 -0
  113. package/dist/utils/validators.js +51 -0
  114. package/dist/utils/validators.js.map +1 -0
  115. package/package.json +70 -0
@@ -0,0 +1,38 @@
1
+ import { Browser, Page } from '@playwright/test';
2
+ import { Config } from '../types/config';
3
+ export declare enum RuntimeMode {
4
+ BROWSER = "browser",
5
+ RECORDER = "recorder"
6
+ }
7
+ export type StartRecorderOptions = {
8
+ captureArtifacts?: boolean;
9
+ };
10
+ export declare class RuntimeController {
11
+ private config;
12
+ private mode;
13
+ private browserAdapter;
14
+ private captureEngine;
15
+ private storageEngine;
16
+ private componentsRegistry;
17
+ private sessionId;
18
+ private capturedPages;
19
+ private pendingCaptures;
20
+ constructor(config: Config, mode: RuntimeMode);
21
+ initialize(): Promise<void>;
22
+ startRecorder(url: string, options?: StartRecorderOptions): Promise<void>;
23
+ private captureFromRecordedScript;
24
+ private executeRecordedSpecSteps;
25
+ private extractTestBody;
26
+ launchBrowser(): Promise<Browser>;
27
+ createContext(browser: Browser): Promise<any>;
28
+ private setupRecorderMode;
29
+ setupPage(page: Page): Promise<void>;
30
+ private normalizeUrl;
31
+ private captureCurrentPage;
32
+ private isValidUrl;
33
+ shutdown(): Promise<void>;
34
+ onBrowserDisconnect(callback: () => void): void;
35
+ getSessionId(): string;
36
+ capturePageIfNeeded(page: Page): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/core/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAU,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAWzC,oBAAY,WAAW;IACrB,OAAO,YAAY;IACnB,QAAQ,aAAa;CACtB;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,kBAAkB,CAA0C;IAEpE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,eAAe,CAAiC;gBAE5C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW;IAavC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAiFrE,yBAAyB;YAYzB,wBAAwB;IAiBtC,OAAO,CAAC,eAAe;IAmCjB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;YAiBrC,iBAAiB;IAmJzB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA8H1C,OAAO,CAAC,YAAY;YAgBN,kBAAkB;IAgGhC,OAAO,CAAC,UAAU;IASZ,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C/B,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAI/C,YAAY,IAAI,MAAM;IAIhB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAsBrD"}
@@ -0,0 +1,648 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RuntimeController = exports.RuntimeMode = void 0;
37
+ // Developer: Shadow Coderr, Architect
38
+ const test_1 = require("@playwright/test");
39
+ const browser_adapter_1 = require("./browser-adapter");
40
+ const capture_engine_1 = require("./capture-engine");
41
+ const engine_1 = require("../storage/engine");
42
+ const components_registry_1 = require("../registry/components-registry");
43
+ const hash_1 = require("../utils/hash");
44
+ const logger_1 = require("../utils/logger");
45
+ const child_process_1 = require("child_process");
46
+ const path = __importStar(require("path"));
47
+ const fs = __importStar(require("fs"));
48
+ var RuntimeMode;
49
+ (function (RuntimeMode) {
50
+ RuntimeMode["BROWSER"] = "browser";
51
+ RuntimeMode["RECORDER"] = "recorder";
52
+ })(RuntimeMode || (exports.RuntimeMode = RuntimeMode = {}));
53
+ class RuntimeController {
54
+ config;
55
+ mode;
56
+ browserAdapter;
57
+ captureEngine;
58
+ storageEngine;
59
+ componentsRegistry = null;
60
+ sessionId;
61
+ capturedPages = new Set();
62
+ pendingCaptures = new Set();
63
+ constructor(config, mode) {
64
+ this.config = config;
65
+ this.mode = mode;
66
+ this.browserAdapter = new browser_adapter_1.BrowserAdapter();
67
+ this.captureEngine = new capture_engine_1.CaptureEngine(config);
68
+ this.storageEngine = new engine_1.StorageEngine(config.storage.outputDir, config.storage.prettyJson, config.capture.forceCapture);
69
+ this.sessionId = (0, hash_1.generateSessionId)(new Date());
70
+ }
71
+ async initialize() {
72
+ await this.storageEngine.initialize();
73
+ // Initialize components registry manager if enabled
74
+ if (this.config.capture.components?.enabled) {
75
+ this.componentsRegistry = new components_registry_1.ComponentsRegistryManager(this.config.storage.outputDir, 'unknown', {
76
+ minOccurrences: this.config.capture.components.minOccurrences,
77
+ maxComponents: this.config.capture.components.maxComponents,
78
+ });
79
+ await this.componentsRegistry.initialize();
80
+ }
81
+ logger_1.logger.info(`Initialized runtime in ${this.mode} mode`);
82
+ }
83
+ async startRecorder(url, options = {}) {
84
+ const scriptPath = await this.storageEngine.getUniqueScriptPath(url);
85
+ logger_1.logger.info(`Starting Playwright Codegen for: ${url}`);
86
+ logger_1.logger.info(`Script will be saved to: ${scriptPath}`);
87
+ return new Promise((resolve, reject) => {
88
+ let cliPath;
89
+ try {
90
+ cliPath = require.resolve('playwright-core/cli');
91
+ }
92
+ catch (e) {
93
+ try {
94
+ cliPath = require.resolve('playwright/cli');
95
+ }
96
+ catch (e2) {
97
+ cliPath = path.join(process.cwd(), 'node_modules', 'playwright-core', 'cli.js');
98
+ }
99
+ }
100
+ logger_1.logger.debug(`Using Playwright CLI at: ${cliPath}`);
101
+ logger_1.logger.debug(`Using Playwright channel: msedge`);
102
+ const env = { ...process.env };
103
+ delete env.NODE_OPTIONS;
104
+ const spawnArgs = [
105
+ cliPath,
106
+ 'codegen',
107
+ '-b', 'chromium',
108
+ '--channel', 'msedge',
109
+ '--output', scriptPath,
110
+ url,
111
+ ];
112
+ logger_1.logger.debug(`Spawning recorder: ${process.execPath} ${spawnArgs.join(' ')}`);
113
+ const recorderProcess = (0, child_process_1.spawn)(process.execPath, spawnArgs, {
114
+ stdio: 'inherit',
115
+ shell: false,
116
+ windowsHide: true,
117
+ cwd: process.cwd(),
118
+ env,
119
+ });
120
+ recorderProcess.on('error', (err) => {
121
+ logger_1.logger.error(`Failed to start recorder: ${err.message}`);
122
+ reject(err);
123
+ });
124
+ recorderProcess.on('close', (code) => {
125
+ if (code === 0) {
126
+ logger_1.logger.info(`Script saved to: ${scriptPath}`);
127
+ }
128
+ else {
129
+ // It's normal for codegen to return non-zero if closed via window X button sometimes
130
+ logger_1.logger.info(`Playwright Codegen session ended (code ${code})`);
131
+ }
132
+ this.storageEngine.mergeRecordedScript(url, scriptPath)
133
+ .then(async (mergedPath) => {
134
+ logger_1.logger.info(`Merged script saved to: ${mergedPath}`);
135
+ if (options.captureArtifacts) {
136
+ try {
137
+ await this.captureFromRecordedScript(mergedPath);
138
+ }
139
+ catch (err) {
140
+ logger_1.logger.warn(`Failed to capture artifacts from recorded script: ${err.message}`);
141
+ }
142
+ }
143
+ resolve();
144
+ })
145
+ .catch((err) => {
146
+ logger_1.logger.warn(`Failed to merge recorded script: ${err.message}`);
147
+ resolve();
148
+ });
149
+ });
150
+ });
151
+ }
152
+ async captureFromRecordedScript(specPath) {
153
+ const browser = await this.launchBrowser();
154
+ const context = await this.createContext(browser);
155
+ const page = await context.newPage();
156
+ await this.setupPage(page);
157
+ await this.executeRecordedSpecSteps(specPath, page);
158
+ await this.shutdown();
159
+ }
160
+ async executeRecordedSpecSteps(specPath, page) {
161
+ const source = fs.readFileSync(specPath, 'utf8');
162
+ const body = this.extractTestBody(source);
163
+ if (!body.trim()) {
164
+ throw new Error(`No executable steps found in script: ${specPath}`);
165
+ }
166
+ const fn = new Function('page', 'expect', `"use strict"; return (async () => {\n${body}\n})();`);
167
+ await fn(page, test_1.expect);
168
+ }
169
+ extractTestBody(specSource) {
170
+ const lines = specSource.split(/\r?\n/);
171
+ const bodyLines = [];
172
+ let inTest = false;
173
+ let startedBody = false;
174
+ for (const line of lines) {
175
+ if (!inTest) {
176
+ if (/^\s*test\(/.test(line)) {
177
+ inTest = true;
178
+ if (line.includes('{')) {
179
+ startedBody = true;
180
+ }
181
+ }
182
+ continue;
183
+ }
184
+ if (!startedBody) {
185
+ if (line.includes('{')) {
186
+ startedBody = true;
187
+ }
188
+ continue;
189
+ }
190
+ if (/^\s*\}\);\s*$/.test(line)) {
191
+ break;
192
+ }
193
+ bodyLines.push(line);
194
+ }
195
+ return bodyLines.join('\n').trim();
196
+ }
197
+ async launchBrowser() {
198
+ return await this.browserAdapter.launchBrowser(this.config);
199
+ }
200
+ async createContext(browser) {
201
+ const context = await this.browserAdapter.createContext(browser, this.config);
202
+ // Attach page handler for newly-created pages (popups)
203
+ context.on('page', async (page) => {
204
+ await this.setupPage(page);
205
+ });
206
+ // Attach setup to any existing pages
207
+ const pages = context.pages();
208
+ for (const p of pages) {
209
+ await this.setupPage(p);
210
+ }
211
+ return context;
212
+ }
213
+ async setupRecorderMode(page) {
214
+ logger_1.logger.info('Setting up recorder mode');
215
+ // Inject script to capture user interactions
216
+ await page.addInitScript(() => {
217
+ // Store recorded interactions
218
+ window.__contextGraph = {
219
+ interactions: [],
220
+ startTime: Date.now()
221
+ };
222
+ // Helper to record interactions
223
+ const recordInteraction = (type, element, data) => {
224
+ const interaction = {
225
+ type,
226
+ timestamp: Date.now(),
227
+ element: {
228
+ tagName: element.tagName.toLowerCase(),
229
+ id: element.id,
230
+ className: element.className,
231
+ text: element.textContent?.slice(0, 50) || '',
232
+ xpath: getXPath(element),
233
+ cssSelector: getCssSelector(element)
234
+ },
235
+ data: data || {}
236
+ };
237
+ window.__contextGraph.interactions.push(interaction);
238
+ console.log('ContextGraph: Recorded', type, interaction);
239
+ };
240
+ // Helper functions
241
+ const getXPath = (element) => {
242
+ if (element.id)
243
+ return `//*[@id="${element.id}"]`;
244
+ if (element === document.body)
245
+ return '/html/body';
246
+ let ix = 0;
247
+ const siblings = element.parentNode.childNodes;
248
+ for (let i = 0; i < siblings.length; i++) {
249
+ const sibling = siblings[i];
250
+ if (sibling === element) {
251
+ return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
252
+ }
253
+ if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
254
+ ix++;
255
+ }
256
+ }
257
+ return '';
258
+ };
259
+ const getCssSelector = (element) => {
260
+ if (element.id)
261
+ return `#${element.id}`;
262
+ if (element.className) {
263
+ const classes = element.className.split(' ').filter((c) => c);
264
+ return `${element.tagName.toLowerCase()}.${classes.join('.')}`;
265
+ }
266
+ return element.tagName.toLowerCase();
267
+ };
268
+ // Record clicks
269
+ document.addEventListener('click', (e) => {
270
+ recordInteraction('click', e.target, {
271
+ button: e.button,
272
+ ctrlKey: e.ctrlKey,
273
+ shiftKey: e.shiftKey,
274
+ altKey: e.altKey
275
+ });
276
+ }, true);
277
+ // Record form inputs
278
+ document.addEventListener('input', (e) => {
279
+ const target = e.target;
280
+ recordInteraction('input', target, {
281
+ value: target.value.slice(0, 100), // Limit value length
282
+ inputType: target.type,
283
+ name: target.name
284
+ });
285
+ }, true);
286
+ // Record focus changes
287
+ document.addEventListener('focus', (e) => {
288
+ recordInteraction('focus', e.target);
289
+ }, true);
290
+ // Record scroll events
291
+ let scrollTimeout;
292
+ window.addEventListener('scroll', () => {
293
+ clearTimeout(scrollTimeout);
294
+ scrollTimeout = setTimeout(() => {
295
+ recordInteraction('scroll', document.body, {
296
+ scrollX: window.scrollX,
297
+ scrollY: window.scrollY
298
+ });
299
+ }, 100);
300
+ });
301
+ // Record navigation
302
+ const originalPushState = history.pushState;
303
+ history.pushState = function (data, unused, url) {
304
+ recordInteraction('navigation', document.body, {
305
+ url: url?.toString(),
306
+ method: 'pushState'
307
+ });
308
+ return originalPushState.apply(this, [data, unused, url]);
309
+ };
310
+ const originalReplaceState = history.replaceState;
311
+ history.replaceState = function (data, unused, url) {
312
+ recordInteraction('navigation', document.body, {
313
+ url: url?.toString(),
314
+ method: 'replaceState'
315
+ });
316
+ return originalReplaceState.apply(this, [data, unused, url]);
317
+ };
318
+ window.addEventListener('popstate', () => {
319
+ recordInteraction('navigation', document.body, {
320
+ url: location.href,
321
+ method: 'popstate'
322
+ });
323
+ });
324
+ });
325
+ // Set up periodic capture of interactions
326
+ const captureInteractions = async () => {
327
+ try {
328
+ const interactions = await page.evaluate(() => window.__contextGraph?.interactions || []);
329
+ if (interactions.length > 0) {
330
+ logger_1.logger.info(`Captured ${interactions.length} user interactions`);
331
+ // Save interactions to a separate file
332
+ await this.storageEngine.saveUserInteractions(page.url(), interactions);
333
+ }
334
+ }
335
+ catch (error) {
336
+ logger_1.logger.debug(`Error capturing interactions: ${error.message}`);
337
+ }
338
+ };
339
+ // Capture interactions every 5 seconds
340
+ const interval = setInterval(captureInteractions, 5000);
341
+ // Clean up on page close
342
+ page.on('close', () => {
343
+ clearInterval(interval);
344
+ captureInteractions(); // Final capture
345
+ });
346
+ }
347
+ async setupPage(page) {
348
+ try {
349
+ // Ensure the page is ready before proceeding
350
+ await page.waitForLoadState('domcontentloaded');
351
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
352
+ logger_1.logger.debug('Network idle state not reached, continuing...');
353
+ });
354
+ // Attach listeners
355
+ await this.browserAdapter.attachPageListeners(page);
356
+ await this.captureEngine.attachNetworkListeners(page);
357
+ // Set up recorder mode if enabled
358
+ if (this.mode === RuntimeMode.RECORDER) {
359
+ await this.setupRecorderMode(page);
360
+ }
361
+ // Prevent accidental closure using addInitScript instead of evaluateOnNewDocument
362
+ await page.addInitScript(() => {
363
+ window.addEventListener('beforeunload', (e) => {
364
+ e.preventDefault();
365
+ // @ts-ignore - TypeScript doesn't recognize returnValue on BeforeUnloadEvent
366
+ e.returnValue = '';
367
+ return '';
368
+ });
369
+ });
370
+ }
371
+ catch (error) {
372
+ logger_1.logger.error(`Error in setupPage: ${error instanceof Error ? error.message : String(error)}`);
373
+ throw error;
374
+ }
375
+ // Set up page change detection
376
+ // Capture traditional load events
377
+ page.on('load', async () => {
378
+ const url = page.url();
379
+ logger_1.logger.info(`Page load detected: ${url}`);
380
+ const key = this.normalizeUrl(url);
381
+ if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
382
+ logger_1.logger.info(`New page detected (load event): ${url}`);
383
+ this.capturedPages.add(key);
384
+ await this.captureCurrentPage(page);
385
+ }
386
+ else {
387
+ logger_1.logger.debug(`Page already captured (load event): ${url}`);
388
+ }
389
+ });
390
+ // Also capture DOMContentLoaded for faster detection
391
+ page.on('domcontentloaded', async () => {
392
+ const url = page.url();
393
+ const key = this.normalizeUrl(url);
394
+ if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
395
+ logger_1.logger.info(`New page detected (DOMContentLoaded): ${url}`);
396
+ this.capturedPages.add(key);
397
+ await this.captureCurrentPage(page);
398
+ }
399
+ });
400
+ // Capture frame navigations (including main frame)
401
+ page.on('framenavigated', async (frame) => {
402
+ // Only capture main frame navigations to avoid duplicates
403
+ if (frame === page.mainFrame()) {
404
+ const url = frame.url();
405
+ logger_1.logger.info(`Frame navigated (main frame): ${url}`);
406
+ const key = this.normalizeUrl(url);
407
+ if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
408
+ logger_1.logger.info(`New page detected (framenavigated): ${url}`);
409
+ this.capturedPages.add(key);
410
+ await this.captureCurrentPage(page);
411
+ }
412
+ else {
413
+ logger_1.logger.debug(`Page already captured (framenavigated): ${url}`);
414
+ }
415
+ }
416
+ });
417
+ // Capture SPA navigation via history API
418
+ try {
419
+ await page.exposeBinding('cc_onHistoryChange', async (_source, url) => {
420
+ logger_1.logger.info(`History change detected: ${url}`);
421
+ const key = this.normalizeUrl(url);
422
+ if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
423
+ logger_1.logger.info(`New page detected (history API): ${url}`);
424
+ this.capturedPages.add(key);
425
+ // Add a small delay to ensure page is ready
426
+ await new Promise(r => setTimeout(r, 300));
427
+ await this.captureCurrentPage(page);
428
+ }
429
+ else {
430
+ logger_1.logger.debug(`Page already captured (history API): ${url}`);
431
+ }
432
+ });
433
+ await page.addInitScript(() => {
434
+ (function () {
435
+ const origPush = history.pushState;
436
+ history.pushState = function () {
437
+ const ret = origPush.apply(this, arguments);
438
+ try {
439
+ setTimeout(() => {
440
+ window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
441
+ }, 100);
442
+ }
443
+ catch (e) { }
444
+ return ret;
445
+ };
446
+ const origReplace = history.replaceState;
447
+ history.replaceState = function () {
448
+ const ret = origReplace.apply(this, arguments);
449
+ try {
450
+ setTimeout(() => {
451
+ window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
452
+ }, 100);
453
+ }
454
+ catch (e) { }
455
+ return ret;
456
+ };
457
+ window.addEventListener('popstate', function () {
458
+ try {
459
+ setTimeout(() => {
460
+ window['cc_onHistoryChange'] && window['cc_onHistoryChange'](location.href);
461
+ }, 100);
462
+ }
463
+ catch (e) { }
464
+ });
465
+ })();
466
+ });
467
+ }
468
+ catch (error) {
469
+ logger_1.logger.warn(`Failed to set up history API interception: ${error.message}`);
470
+ }
471
+ }
472
+ normalizeUrl(url) {
473
+ try {
474
+ const u = new URL(url);
475
+ // Sort query params for consistent comparison
476
+ const sortedParams = Array.from(u.searchParams.entries())
477
+ .sort((a, b) => a[0].localeCompare(b[0]));
478
+ const queryString = sortedParams.length > 0
479
+ ? '?' + sortedParams.map(([k, v]) => `${k}=${v}`).join('&')
480
+ : '';
481
+ // Fix: Include hash so SPAs (e.g., /#/dashboard) are treated as unique pages
482
+ return `${u.origin}${u.pathname}${queryString}${u.hash}`;
483
+ }
484
+ catch {
485
+ return url;
486
+ }
487
+ }
488
+ async captureCurrentPage(page) {
489
+ const captureWork = (async () => {
490
+ const maxAttempts = 3;
491
+ let attempt = 0;
492
+ const startingUrl = page.url();
493
+ while (attempt < maxAttempts) {
494
+ attempt++;
495
+ try {
496
+ if (page.isClosed()) {
497
+ logger_1.logger.warn('Skipping capture: page is already closed');
498
+ break;
499
+ }
500
+ // If the page has navigated since this capture was scheduled, abort to avoid racing navigation.
501
+ const liveUrl = page.url();
502
+ if (this.normalizeUrl(liveUrl) !== this.normalizeUrl(startingUrl)) {
503
+ logger_1.logger.info(`Skipping capture: page navigated during capture scheduling (${startingUrl} -> ${liveUrl})`);
504
+ break;
505
+ }
506
+ // Wait for page to be stable
507
+ try {
508
+ await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {
509
+ logger_1.logger.debug('Network idle timeout, proceeding with capture');
510
+ });
511
+ }
512
+ catch {
513
+ await new Promise(r => setTimeout(r, 1000));
514
+ }
515
+ if (page.isClosed()) {
516
+ logger_1.logger.warn('Skipping capture: page closed while waiting for stability');
517
+ break;
518
+ }
519
+ const currentUrl = page.url();
520
+ logger_1.logger.info(`Capturing page (attempt ${attempt}/${maxAttempts}): ${currentUrl}`);
521
+ const consoleMessages = this.browserAdapter.getConsoleMessages();
522
+ const snapshot = await this.captureEngine.capturePageSnapshot(page, this.config, consoleMessages);
523
+ await this.storageEngine.savePageSnapshot(snapshot);
524
+ // Update components registry
525
+ if (this.componentsRegistry && this.config.capture.components?.enabled) {
526
+ // Update registry domain if unknown
527
+ if (this.componentsRegistry.registry?.domain === 'unknown') {
528
+ this.componentsRegistry.registry.domain = snapshot.metadata.domain;
529
+ }
530
+ await this.componentsRegistry.processPage(snapshot);
531
+ }
532
+ this.browserAdapter.clearConsoleMessages();
533
+ await this.storageEngine.updateGlobalManifest({
534
+ captureId: snapshot.metadata.captureId,
535
+ url: snapshot.metadata.url,
536
+ title: snapshot.metadata.title,
537
+ timestamp: snapshot.metadata.timestamp,
538
+ sessionId: this.sessionId,
539
+ domain: snapshot.metadata.domain,
540
+ mode: this.mode,
541
+ });
542
+ logger_1.logger.info(`✓ Successfully captured page: ${snapshot.metadata.url} (saved as: ${snapshot.metadata.pageName})`);
543
+ break;
544
+ }
545
+ catch (error) {
546
+ const msg = error.message || '';
547
+ logger_1.logger.warn(`Capture attempt ${attempt} failed: ${msg}`);
548
+ // Check for fatal browser closure errors - don't retry these
549
+ if (/Target page, context or browser has been closed|Execution context was destroyed|Navigation cancelled/i.test(msg)) {
550
+ logger_1.logger.warn(`Capture aborted due to navigation/page close: ${msg}`);
551
+ break; // don't retry these
552
+ }
553
+ // For other errors, retry if we haven't exhausted attempts
554
+ if (attempt < maxAttempts) {
555
+ await new Promise((r) => setTimeout(r, 300));
556
+ continue;
557
+ }
558
+ logger_1.logger.error(`Failed to capture page: ${error}`);
559
+ break;
560
+ }
561
+ }
562
+ })();
563
+ this.pendingCaptures.add(captureWork);
564
+ try {
565
+ await captureWork;
566
+ }
567
+ finally {
568
+ this.pendingCaptures.delete(captureWork);
569
+ }
570
+ }
571
+ isValidUrl(url) {
572
+ try {
573
+ const parsed = new URL(url);
574
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
575
+ }
576
+ catch {
577
+ return false;
578
+ }
579
+ }
580
+ async shutdown() {
581
+ try {
582
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), 20000));
583
+ await Promise.race([
584
+ Promise.all(Array.from(this.pendingCaptures)),
585
+ timeout
586
+ ]);
587
+ logger_1.logger.info('All pending captures completed');
588
+ }
589
+ catch (err) {
590
+ const error = err;
591
+ if (error.message === 'Shutdown timeout') {
592
+ logger_1.logger.warn('Shutdown timeout - some captures may still be in progress');
593
+ }
594
+ else {
595
+ logger_1.logger.warn('Error waiting for pending captures: ' + error.message);
596
+ }
597
+ }
598
+ // Persist components registry and update manifest reference
599
+ try {
600
+ if (this.componentsRegistry && this.config.capture.components?.enabled) {
601
+ const domain = this.componentsRegistry.getRegistry().domain;
602
+ const domainName = (() => {
603
+ const parts = domain.split('.');
604
+ const filtered = parts.filter(p => p.toLowerCase() !== 'www');
605
+ if (filtered.length >= 2)
606
+ return filtered[filtered.length - 2];
607
+ return filtered[0] || parts[0];
608
+ })();
609
+ const registry = this.componentsRegistry.getRegistry();
610
+ await this.storageEngine.saveComponentsRegistry(registry, domainName);
611
+ await this.storageEngine.updateManifestWithComponents(domainName, registry.components.length);
612
+ }
613
+ }
614
+ catch (error) {
615
+ logger_1.logger.warn(`Failed to persist components registry: ${error.message}`);
616
+ }
617
+ await this.browserAdapter.close();
618
+ logger_1.logger.info('Runtime shutdown complete');
619
+ }
620
+ onBrowserDisconnect(callback) {
621
+ this.browserAdapter.onBrowserDisconnect(callback);
622
+ }
623
+ getSessionId() {
624
+ return this.sessionId;
625
+ }
626
+ async capturePageIfNeeded(page) {
627
+ try {
628
+ const url = page.url();
629
+ const key = this.normalizeUrl(url);
630
+ if (!this.capturedPages.has(key) && this.isValidUrl(url)) {
631
+ logger_1.logger.info(`New page detected: ${url}`);
632
+ // Ensure page is stable before capturing
633
+ await page.waitForLoadState('domcontentloaded');
634
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
635
+ logger_1.logger.debug('Network idle state not reached, continuing with capture...');
636
+ });
637
+ await this.captureCurrentPage(page);
638
+ }
639
+ }
640
+ catch (error) {
641
+ const errorMessage = error instanceof Error ? error.message : String(error);
642
+ logger_1.logger.error(`Error in capturePageIfNeeded: ${errorMessage}`);
643
+ throw error;
644
+ }
645
+ }
646
+ }
647
+ exports.RuntimeController = RuntimeController;
648
+ //# sourceMappingURL=runtime.js.map