@aryanduntley/pwa-debug 0.1.7 → 0.1.8
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/dist/main.js +86 -0
- package/extension/service-worker.js +150 -0
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -14871,6 +14871,90 @@ const registerChromeDevtoolsTool = Object.freeze({
|
|
|
14871
14871
|
handler: registerChromeDevtoolsHandler,
|
|
14872
14872
|
});
|
|
14873
14873
|
|
|
14874
|
+
// The SW waits up to timeout_ms (capped below) for the page to finish loading
|
|
14875
|
+
// before replying, so the IPC budget must comfortably exceed it.
|
|
14876
|
+
const NAV_MAX_LOAD_TIMEOUT_MS = 12_000;
|
|
14877
|
+
const NAV_IPC_TIMEOUT_MS = NAV_MAX_LOAD_TIMEOUT_MS + 3_000;
|
|
14878
|
+
const navigateInputSchema = {
|
|
14879
|
+
extension_id: stringType().min(1).optional(),
|
|
14880
|
+
tab_id: numberType().int().optional(),
|
|
14881
|
+
url: stringType().min(1),
|
|
14882
|
+
timeout_ms: numberType().int().positive().max(NAV_MAX_LOAD_TIMEOUT_MS).optional(),
|
|
14883
|
+
};
|
|
14884
|
+
const newTabInputSchema = {
|
|
14885
|
+
extension_id: stringType().min(1).optional(),
|
|
14886
|
+
url: stringType().min(1),
|
|
14887
|
+
active: booleanType().optional(),
|
|
14888
|
+
timeout_ms: numberType().int().positive().max(NAV_MAX_LOAD_TIMEOUT_MS).optional(),
|
|
14889
|
+
};
|
|
14890
|
+
/**
|
|
14891
|
+
* Shared IPC round-trip for the navigation tools: resolve the target NMH, send a
|
|
14892
|
+
* single request envelope to the SW, and normalize transport vs. handler errors.
|
|
14893
|
+
* The SW handler (request_router) drives chrome.tabs and waits for load-complete.
|
|
14894
|
+
*/
|
|
14895
|
+
const sendNavRequest = async (ctx, extensionId, tool, payload, nextSteps) => {
|
|
14896
|
+
const target = resolveTarget$1(ctx, extensionId);
|
|
14897
|
+
if (!target.ok) {
|
|
14898
|
+
return errorResponse(target.error, [
|
|
14899
|
+
'Call host_status to see activeConnections. If empty, ensure host_register_extension has been called and the extension reloaded at chrome://extensions.',
|
|
14900
|
+
]);
|
|
14901
|
+
}
|
|
14902
|
+
const env = Object.freeze({
|
|
14903
|
+
type: 'request',
|
|
14904
|
+
requestId: randomUUID(),
|
|
14905
|
+
tool,
|
|
14906
|
+
extensionId: target.extensionId,
|
|
14907
|
+
payload,
|
|
14908
|
+
});
|
|
14909
|
+
let response;
|
|
14910
|
+
try {
|
|
14911
|
+
response = await ctx.ipcServer.request(target.extensionId, env, {
|
|
14912
|
+
timeoutMs: NAV_IPC_TIMEOUT_MS,
|
|
14913
|
+
});
|
|
14914
|
+
}
|
|
14915
|
+
catch (err) {
|
|
14916
|
+
return errorResponse(`${tool} failed: ${err.message}`, [
|
|
14917
|
+
`IPC request did not complete. Confirm the SW is connected and the ${tool} handler is wired in the SW.`,
|
|
14918
|
+
]);
|
|
14919
|
+
}
|
|
14920
|
+
if (response.error) {
|
|
14921
|
+
return errorResponse(`${tool} nmh error: ${response.error.message}`, [
|
|
14922
|
+
'The SW rejected the navigation (invalid URL, no active tab, or chrome.tabs threw). Pass an explicit tab_id, or open a tab first.',
|
|
14923
|
+
]);
|
|
14924
|
+
}
|
|
14925
|
+
const data = {
|
|
14926
|
+
extensionId: target.extensionId,
|
|
14927
|
+
...response.payload,
|
|
14928
|
+
};
|
|
14929
|
+
return okResponse(data, [...nextSteps]);
|
|
14930
|
+
};
|
|
14931
|
+
const navigateHandler = async (args, ctx) => sendNavRequest(ctx, args.extension_id, 'pdl_navigate', {
|
|
14932
|
+
url: args.url,
|
|
14933
|
+
...(args.tab_id !== undefined ? { tab_id: args.tab_id } : {}),
|
|
14934
|
+
...(args.timeout_ms !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
14935
|
+
}, [
|
|
14936
|
+
'Navigated the active (or given) tab. Returns { tabId, url, status }: status "complete" = page finished loading; "loading" = navigation happened but the load was still in flight when the wait elapsed (raise timeout_ms, or poll session_ping). pwa-debug capture/inspection tools now target the new page.',
|
|
14937
|
+
]);
|
|
14938
|
+
const newTabHandler = async (args, ctx) => sendNavRequest(ctx, args.extension_id, 'pdl_new_tab', {
|
|
14939
|
+
url: args.url,
|
|
14940
|
+
...(args.active !== undefined ? { active: args.active } : {}),
|
|
14941
|
+
...(args.timeout_ms !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
14942
|
+
}, [
|
|
14943
|
+
'Opened a new tab. Returns { tabId, url, status, created:true }. Pass the returned tabId to other pwa-debug tools (react_tree, console_tail, pdl_click, …) to target this tab; without tab_id they use the active tab.',
|
|
14944
|
+
]);
|
|
14945
|
+
const navigateTool = Object.freeze({
|
|
14946
|
+
name: 'pdl_navigate',
|
|
14947
|
+
description: "Navigate a browser tab to a URL, driven through the pwa-debug extension's service worker (chrome.tabs.update) — NO CDP, so it works on the user's real, logged-in profile without chrome-devtools-mcp being registered or attached. Targets the active tab in the last-focused window unless tab_id is given. The URL may omit the scheme (https:// is assumed); a javascript: URL is rejected. Waits for the page to reach document 'complete' (up to timeout_ms, default 10000, max 12000) before returning { extensionId, tabId, url, status, windowId? }, where status is 'complete' or 'loading' (load still in flight at timeout — the navigation still happened). Args: { url: non-empty string, tab_id?, timeout_ms?, extension_id? }. With no extension_id/tab_id, targets the single connected NMH and the active tab. CALL host_status FIRST to confirm a connection.",
|
|
14948
|
+
inputSchema: navigateInputSchema,
|
|
14949
|
+
handler: navigateHandler,
|
|
14950
|
+
});
|
|
14951
|
+
const newTabTool = Object.freeze({
|
|
14952
|
+
name: 'pdl_new_tab',
|
|
14953
|
+
description: "Open a NEW browser tab at a URL via the pwa-debug extension's service worker (chrome.tabs.create) — NO CDP, works on the user's real profile with no chrome-devtools-mcp dependency. The URL may omit the scheme (https:// is assumed); a javascript: URL is rejected. active? controls foreground vs. background (defaults to foreground). Waits for the page to reach document 'complete' (up to timeout_ms, default 10000, max 12000) before returning { extensionId, tabId, url, status, windowId?, created:true }. Pass the returned tabId to other pwa-debug tools to target this tab specifically. Args: { url: non-empty string, active?, timeout_ms?, extension_id? }. With no extension_id, targets the single connected NMH. CALL host_status FIRST to confirm a connection.",
|
|
14954
|
+
inputSchema: newTabInputSchema,
|
|
14955
|
+
handler: newTabHandler,
|
|
14956
|
+
});
|
|
14957
|
+
|
|
14874
14958
|
const ACTION_IPC_TIMEOUT_MS = 5000;
|
|
14875
14959
|
// Locator + routing fields shared by every action tool.
|
|
14876
14960
|
const locatorSchema = {
|
|
@@ -15041,6 +15125,8 @@ const TOOLS = Object.freeze([
|
|
|
15041
15125
|
storageGetTool,
|
|
15042
15126
|
idbListTool,
|
|
15043
15127
|
idbQueryTool,
|
|
15128
|
+
navigateTool,
|
|
15129
|
+
newTabTool,
|
|
15044
15130
|
launchBrowserTool,
|
|
15045
15131
|
browserStatusTool,
|
|
15046
15132
|
closeBrowserTool,
|
|
@@ -176,6 +176,110 @@ const dispatchToTabClassified = async (tabId, req, opts = {}) => {
|
|
|
176
176
|
return { ok: true, response };
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
+
// SW-side navigation primitives — wraps chrome.tabs URL navigation so the request
|
|
180
|
+
// router stays a thin orchestrator. SW-handled (NOT page-world): chrome.tabs is
|
|
181
|
+
// only reachable in the extension service worker, and driving the tab to a new URL
|
|
182
|
+
// is a browser action, not an in-page read. This is the pwa-debug counterpart to
|
|
183
|
+
// chrome-devtools-mcp's navigate_page/new_page — it works through the loaded
|
|
184
|
+
// extension on the user's real profile, with no CDP attach required.
|
|
185
|
+
const DEFAULT_LOAD_TIMEOUT_MS = 10_000;
|
|
186
|
+
// A bare javascript: URL would run script in the target page context on navigate;
|
|
187
|
+
// reject it. Everything else (http(s), file:, about:, data:, chrome:, localhost
|
|
188
|
+
// shorthand) is allowed — this is a debugging driver, not a sandbox.
|
|
189
|
+
const JAVASCRIPT_SCHEME = /^javascript:/i;
|
|
190
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
191
|
+
/**
|
|
192
|
+
* Normalize a caller-supplied URL: trim, reject empty / javascript:, and assume
|
|
193
|
+
* https:// when no scheme is given (so `example.com` works). Returns null when the
|
|
194
|
+
* input is unusable so the caller can surface a clear error.
|
|
195
|
+
*/
|
|
196
|
+
const normalizeUrl = (raw) => {
|
|
197
|
+
const trimmed = raw.trim();
|
|
198
|
+
if (trimmed.length === 0)
|
|
199
|
+
return null;
|
|
200
|
+
if (JAVASCRIPT_SCHEME.test(trimmed))
|
|
201
|
+
return null;
|
|
202
|
+
return HAS_SCHEME.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
203
|
+
};
|
|
204
|
+
/** Active tab id in the last-focused window, or undefined when there is none. */
|
|
205
|
+
const activeTabId = async () => {
|
|
206
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
207
|
+
return tabs[0]?.id;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Resolve once the tab reports document 'complete', or 'timeout' after timeoutMs.
|
|
211
|
+
* The onUpdated listener is added BEFORE the caller triggers the load so the
|
|
212
|
+
* 'complete' event can't be missed; it is always removed and the timer cleared.
|
|
213
|
+
*/
|
|
214
|
+
const waitForComplete = (tabId, timeoutMs) => new Promise((resolve) => {
|
|
215
|
+
let settled = false;
|
|
216
|
+
const settle = (outcome) => {
|
|
217
|
+
if (settled)
|
|
218
|
+
return;
|
|
219
|
+
settled = true;
|
|
220
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
resolve(outcome);
|
|
223
|
+
};
|
|
224
|
+
const onUpdated = (id, info) => {
|
|
225
|
+
if (id === tabId && info.status === 'complete')
|
|
226
|
+
settle('complete');
|
|
227
|
+
};
|
|
228
|
+
const timer = setTimeout(() => settle('timeout'), timeoutMs);
|
|
229
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* Navigate the active (or given) tab to url and wait for it to finish loading.
|
|
233
|
+
* Throws on an invalid url or when no tab can be resolved. The listener is armed
|
|
234
|
+
* before chrome.tabs.update so the fresh load's 'complete' is observed.
|
|
235
|
+
*/
|
|
236
|
+
const navigateTab = async (input) => {
|
|
237
|
+
const url = normalizeUrl(input.url);
|
|
238
|
+
if (url === null) {
|
|
239
|
+
throw new Error(`navigate: invalid or unsupported url: ${JSON.stringify(input.url)}`);
|
|
240
|
+
}
|
|
241
|
+
const tabId = input.tabId ?? (await activeTabId());
|
|
242
|
+
if (tabId === undefined) {
|
|
243
|
+
throw new Error('navigate: no active tab (open a tab or pass tab_id)');
|
|
244
|
+
}
|
|
245
|
+
const pending = waitForComplete(tabId, input.timeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);
|
|
246
|
+
const tab = await chrome.tabs.update(tabId, { url });
|
|
247
|
+
const outcome = await pending;
|
|
248
|
+
return {
|
|
249
|
+
tabId,
|
|
250
|
+
url,
|
|
251
|
+
...(tab?.windowId !== undefined ? { windowId: tab.windowId } : {}),
|
|
252
|
+
status: outcome === 'complete' ? 'complete' : 'loading',
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Open a new tab at url and wait for it to finish loading. active defaults to
|
|
257
|
+
* Chrome's behavior (foreground). Throws on an invalid url or when Chrome returns
|
|
258
|
+
* no tab id.
|
|
259
|
+
*/
|
|
260
|
+
const openNewTab = async (input) => {
|
|
261
|
+
const url = normalizeUrl(input.url);
|
|
262
|
+
if (url === null) {
|
|
263
|
+
throw new Error(`new_tab: invalid or unsupported url: ${JSON.stringify(input.url)}`);
|
|
264
|
+
}
|
|
265
|
+
const tab = await chrome.tabs.create({
|
|
266
|
+
url,
|
|
267
|
+
...(input.active !== undefined ? { active: input.active } : {}),
|
|
268
|
+
});
|
|
269
|
+
const tabId = tab.id;
|
|
270
|
+
if (tabId === undefined) {
|
|
271
|
+
throw new Error('new_tab: chrome did not return a tab id');
|
|
272
|
+
}
|
|
273
|
+
const outcome = await waitForComplete(tabId, input.timeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);
|
|
274
|
+
return {
|
|
275
|
+
tabId,
|
|
276
|
+
url,
|
|
277
|
+
...(tab.windowId !== undefined ? { windowId: tab.windowId } : {}),
|
|
278
|
+
status: outcome === 'complete' ? 'complete' : 'loading',
|
|
279
|
+
created: true,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
179
283
|
const compilePatternList = (sources, fieldPathPrefix) => {
|
|
180
284
|
if (sources === undefined || sources.length === 0) {
|
|
181
285
|
return { ok: true, value: [] };
|
|
@@ -1610,6 +1714,50 @@ const handleSessionRecord = async (env) => {
|
|
|
1610
1714
|
}
|
|
1611
1715
|
return response.payload;
|
|
1612
1716
|
};
|
|
1717
|
+
// --- Navigation tools (pdl_navigate / pdl_new_tab) ---------------------------
|
|
1718
|
+
// SW-handled, NOT page-world: chrome.tabs drives the browser to a URL. Forwarded
|
|
1719
|
+
// to the sw_navigation module, which validates the URL and waits for load.
|
|
1720
|
+
const readTimeoutMs = (r) => typeof r['timeout_ms'] === 'number' &&
|
|
1721
|
+
Number.isInteger(r['timeout_ms']) &&
|
|
1722
|
+
r['timeout_ms'] > 0
|
|
1723
|
+
? r['timeout_ms']
|
|
1724
|
+
: undefined;
|
|
1725
|
+
const handleNavigate = async (env) => {
|
|
1726
|
+
const raw = env.payload;
|
|
1727
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1728
|
+
throw new Error('pdl_navigate: payload must be { url: non-empty string, tab_id?, timeout_ms? }');
|
|
1729
|
+
}
|
|
1730
|
+
const r = raw;
|
|
1731
|
+
const url = r['url'];
|
|
1732
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
1733
|
+
throw new Error('pdl_navigate: payload must include { url: non-empty string }');
|
|
1734
|
+
}
|
|
1735
|
+
const tabId = readTabId(raw);
|
|
1736
|
+
const timeoutMs = readTimeoutMs(r);
|
|
1737
|
+
return navigateTab({
|
|
1738
|
+
url,
|
|
1739
|
+
...(tabId !== undefined ? { tabId } : {}),
|
|
1740
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1741
|
+
});
|
|
1742
|
+
};
|
|
1743
|
+
const handleNewTab = async (env) => {
|
|
1744
|
+
const raw = env.payload;
|
|
1745
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1746
|
+
throw new Error('pdl_new_tab: payload must be { url: non-empty string, active?, timeout_ms? }');
|
|
1747
|
+
}
|
|
1748
|
+
const r = raw;
|
|
1749
|
+
const url = r['url'];
|
|
1750
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
1751
|
+
throw new Error('pdl_new_tab: payload must include { url: non-empty string }');
|
|
1752
|
+
}
|
|
1753
|
+
const active = typeof r['active'] === 'boolean' ? r['active'] : undefined;
|
|
1754
|
+
const timeoutMs = readTimeoutMs(r);
|
|
1755
|
+
return openNewTab({
|
|
1756
|
+
url,
|
|
1757
|
+
...(active !== undefined ? { active } : {}),
|
|
1758
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1759
|
+
});
|
|
1760
|
+
};
|
|
1613
1761
|
// --- Path 7 interaction action tools (pdl_*) ---------------------------------
|
|
1614
1762
|
// One generic handler per ACTION_TOOL_SPECS entry: extract tab_id for routing,
|
|
1615
1763
|
// forward the locator + params payload to the page-world unchanged.
|
|
@@ -1643,6 +1791,8 @@ const makeActionRequestHandler = (tool) => async (env) => {
|
|
|
1643
1791
|
const actionRequestHandlers = Object.freeze(Object.fromEntries(ACTION_TOOL_SPECS.map((s) => [s.tool, makeActionRequestHandler(s.tool)])));
|
|
1644
1792
|
const HANDLERS = Object.freeze({
|
|
1645
1793
|
...actionRequestHandlers,
|
|
1794
|
+
pdl_navigate: handleNavigate,
|
|
1795
|
+
pdl_new_tab: handleNewTab,
|
|
1646
1796
|
session_ping: handleSessionPing,
|
|
1647
1797
|
recent_events: handleRecentEvents,
|
|
1648
1798
|
evaluate: handleEvaluate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryanduntley/pwa-debug",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@types/winreg": "^1.2.36",
|
|
64
64
|
"rollup": "^4.27.4",
|
|
65
65
|
"tslib": "^2.8.1",
|
|
66
|
-
"@pwa-debug/
|
|
67
|
-
"@pwa-debug/
|
|
66
|
+
"@pwa-debug/extension": "0.1.8",
|
|
67
|
+
"@pwa-debug/shared": "0.1.8"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"typecheck": "tsc --noEmit",
|