@capybara-devtools/web 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +130 -0
- package/dist/storage.js.map +1 -0
- package/package.json +50 -4
- package/src/errors.ts +26 -0
- package/src/index.ts +103 -0
- package/src/storage.ts +130 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lhphuc99
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @capybara-devtools/web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser SDK for [Capybara](https://github.com/lhphuc99/capybara#readme): sends your app's logs, network requests (including WebSockets) and storage to the Capybara UI on your computer. Works with React, Vite and the client side of Next.js.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @capybara-devtools/web
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Import it before the rest of your app so early logs and requests are captured:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { init } from '@capybara-devtools/web';
|
|
13
|
+
|
|
14
|
+
if (import.meta.env.DEV) init({ appName: 'My web app' });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In Next.js 15.3 or later, put it in `instrumentation-client.ts` at the project root:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { init } from '@capybara-devtools/web';
|
|
21
|
+
|
|
22
|
+
if (process.env.NODE_ENV === 'development') init({ appName: 'My Next.js app' });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run `npx capybara-devtools` (or open the desktop app) to see the data.
|
|
26
|
+
|
|
27
|
+
Documentation: [github.com/lhphuc99/capybara](https://github.com/lhphuc99/capybara#web-react-vite-nextjs-client)
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,wBAAgB,kBAAkB,IAAI,MAAM,CAuB3C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function windowErrorsPlugin() {
|
|
2
|
+
return {
|
|
3
|
+
name: 'errors',
|
|
4
|
+
setup(ctx) {
|
|
5
|
+
// Bubbling listener only: resource load failures (broken <img>) don't bubble and stay out.
|
|
6
|
+
const onError = (event) => {
|
|
7
|
+
const args = event.error !== undefined && event.error !== null
|
|
8
|
+
? [event.error]
|
|
9
|
+
: [event.message, `at ${event.filename}:${event.lineno}:${event.colno}`];
|
|
10
|
+
ctx.emit('log', { level: 'error', source: 'uncaught', args: args.map((arg) => ctx.serialize(arg)) });
|
|
11
|
+
};
|
|
12
|
+
const onRejection = (event) => {
|
|
13
|
+
ctx.emit('log', { level: 'error', source: 'unhandledrejection', args: [ctx.serialize(event.reason)] });
|
|
14
|
+
};
|
|
15
|
+
window.addEventListener('error', onError);
|
|
16
|
+
window.addEventListener('unhandledrejection', onRejection);
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener('error', onError);
|
|
19
|
+
window.removeEventListener('unhandledrejection', onRejection);
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB;IAChC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK,CAAC,GAAG;YACP,2FAA2F;YAC3F,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;gBACpC,MAAM,IAAI,GACR,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI;oBAC/C,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;oBACf,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;gBAC7E,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;YACvG,CAAC,CAAC;YACF,MAAM,WAAW,GAAG,CAAC,KAA4B,EAAE,EAAE;gBACnD,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;YACzG,CAAC,CAAC;YACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC1C,MAAM,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;YAC3D,OAAO,GAAG,EAAE;gBACV,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC7C,MAAM,CAAC,mBAAmB,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;YAChE,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CapybaraClient, type NetworkPluginOptions, type Plugin, type StorageAdapter } from '@capybara-devtools/core';
|
|
2
|
+
export declare const SDK_VERSION = "0.2.0";
|
|
3
|
+
export interface InitOptions {
|
|
4
|
+
/** Shown in the session picker. Defaults to the page title. */
|
|
5
|
+
appName?: string;
|
|
6
|
+
/** Machine running `capybara`. Defaults to the hostname the page was loaded from. */
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
/** Pass false to skip setup, e.g. `enabled: import.meta.env.DEV`. */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** Capture console output and uncaught errors. Default true. */
|
|
12
|
+
logs?: boolean;
|
|
13
|
+
network?: boolean | NetworkPluginOptions;
|
|
14
|
+
/** `false` turns storage off; an array adds adapters next to localStorage, sessionStorage and cookies. */
|
|
15
|
+
storage?: boolean | StorageAdapter[];
|
|
16
|
+
plugins?: Plugin[];
|
|
17
|
+
}
|
|
18
|
+
export declare function init(options?: InitOptions): CapybaraClient | undefined;
|
|
19
|
+
export { getActiveClient } from '@capybara-devtools/core';
|
|
20
|
+
export type { CapybaraClient, NetworkPluginOptions, Plugin, StorageAdapter } from '@capybara-devtools/core';
|
|
21
|
+
export { cookieStorage, webStorage } from './storage.js';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACX,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAKjC,eAAO,MAAM,WAAW,UAAU,CAAC;AAOnC,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IACzC,0GAA0G;IAC1G,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,cAAc,GAAG,SAAS,CAiC1E;AA6BD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { consolePlugin, createClient, getActiveClient, networkPlugin, storagePlugin, } from '@capybara-devtools/core';
|
|
2
|
+
import { CLIENT_PATH, DEFAULT_PORT } from '@capybara-devtools/protocol';
|
|
3
|
+
import { windowErrorsPlugin } from './errors.js';
|
|
4
|
+
import { cookieStorage, webStorage } from './storage.js';
|
|
5
|
+
export const SDK_VERSION = '0.2.0';
|
|
6
|
+
/** Traffic of a dev server's own tooling: Next.js error overlay requests and its HMR socket. */
|
|
7
|
+
const DEV_SERVER_NOISE = [/\/__nextjs_[\w-]+(\?|$)/, /\/_next\/webpack-hmr(\?|$)/];
|
|
8
|
+
/** React's development build prints this on every page when the React DevTools extension is missing. */
|
|
9
|
+
const CONSOLE_NOISE = [/Download the React DevTools for a better development experience/];
|
|
10
|
+
export function init(options = {}) {
|
|
11
|
+
// The window check makes this a no-op during server rendering, so the same entry works in Next.js.
|
|
12
|
+
if (options.enabled === false || typeof window === 'undefined')
|
|
13
|
+
return undefined;
|
|
14
|
+
// Hot reload re-runs the entry file; patching again would record everything twice.
|
|
15
|
+
const existing = getActiveClient();
|
|
16
|
+
if (existing)
|
|
17
|
+
return existing;
|
|
18
|
+
const plugins = [];
|
|
19
|
+
if (options.logs !== false)
|
|
20
|
+
plugins.push(consolePlugin({ ignore: CONSOLE_NOISE }), windowErrorsPlugin());
|
|
21
|
+
if (options.network !== false) {
|
|
22
|
+
const network = typeof options.network === 'object' ? options.network : {};
|
|
23
|
+
plugins.push(networkPlugin({ ...network, ignoreUrls: [...DEV_SERVER_NOISE, ...metroSockets(), ...(network.ignoreUrls ?? [])] }));
|
|
24
|
+
}
|
|
25
|
+
if (options.storage !== false) {
|
|
26
|
+
const extra = Array.isArray(options.storage) ? options.storage : [];
|
|
27
|
+
plugins.push(storagePlugin([...defaultStorages(), ...extra]));
|
|
28
|
+
}
|
|
29
|
+
plugins.push(...(options.plugins ?? []));
|
|
30
|
+
const host = options.host ?? (window.location.hostname || 'localhost');
|
|
31
|
+
return createClient({
|
|
32
|
+
url: `ws://${formatHost(host)}:${options.port ?? DEFAULT_PORT}${CLIENT_PATH}`,
|
|
33
|
+
session: {
|
|
34
|
+
appName: options.appName ?? (document.title || window.location.host || 'Web app'),
|
|
35
|
+
platform: 'web',
|
|
36
|
+
sdk: { name: '@capybara-devtools/web', version: SDK_VERSION },
|
|
37
|
+
url: window.location.href,
|
|
38
|
+
userAgent: navigator.userAgent,
|
|
39
|
+
},
|
|
40
|
+
plugins,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function defaultStorages() {
|
|
44
|
+
const adapters = [];
|
|
45
|
+
// Reading these throws in sandboxed iframes and when site data is blocked.
|
|
46
|
+
try {
|
|
47
|
+
adapters.push(webStorage(window.localStorage, 'localStorage', 'Local Storage'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// unavailable
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
adapters.push(webStorage(window.sessionStorage, 'sessionStorage', 'Session Storage'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// unavailable
|
|
57
|
+
}
|
|
58
|
+
adapters.push(cookieStorage());
|
|
59
|
+
return adapters;
|
|
60
|
+
}
|
|
61
|
+
/** With Expo web, Metro serves the page and also opens /hot and /message sockets on the same origin. */
|
|
62
|
+
function metroSockets() {
|
|
63
|
+
const host = window.location.host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
+
return host ? [new RegExp(`^wss?://${host}/(?:hot|message)(?:[/?]|$)`)] : [];
|
|
65
|
+
}
|
|
66
|
+
function formatHost(host) {
|
|
67
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
68
|
+
}
|
|
69
|
+
export { getActiveClient } from '@capybara-devtools/core';
|
|
70
|
+
export { cookieStorage, webStorage } from './storage.js';
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAyC9D,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAkCtF;AAED,wBAAgB,aAAa,IAAI,cAAc,CA0B9C"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const listeners = new Map();
|
|
2
|
+
let prototypePatched = false;
|
|
3
|
+
// Patched once and never undone: with no listeners it costs nothing, and undoing it
|
|
4
|
+
// could remove a wrapper another library installed on top of ours.
|
|
5
|
+
function patchStoragePrototype() {
|
|
6
|
+
if (prototypePatched || typeof Storage === 'undefined')
|
|
7
|
+
return;
|
|
8
|
+
prototypePatched = true;
|
|
9
|
+
const proto = Storage.prototype;
|
|
10
|
+
const { setItem, removeItem, clear } = proto;
|
|
11
|
+
proto.setItem = function (key, value) {
|
|
12
|
+
setItem.call(this, key, value);
|
|
13
|
+
notify(this, 'set', String(key));
|
|
14
|
+
};
|
|
15
|
+
proto.removeItem = function (key) {
|
|
16
|
+
removeItem.call(this, key);
|
|
17
|
+
notify(this, 'remove', String(key));
|
|
18
|
+
};
|
|
19
|
+
proto.clear = function () {
|
|
20
|
+
clear.call(this);
|
|
21
|
+
notify(this, 'clear');
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function notify(storage, op, key) {
|
|
25
|
+
const bucket = listeners.get(storage);
|
|
26
|
+
if (!bucket)
|
|
27
|
+
return;
|
|
28
|
+
for (const listener of bucket) {
|
|
29
|
+
try {
|
|
30
|
+
listener(op, key);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// a broken listener must not break the app's write
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function webStorage(storage, id, label) {
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
label,
|
|
41
|
+
keys: () => {
|
|
42
|
+
const keys = [];
|
|
43
|
+
for (let i = 0; i < storage.length; i++) {
|
|
44
|
+
const key = storage.key(i);
|
|
45
|
+
if (key !== null)
|
|
46
|
+
keys.push(key);
|
|
47
|
+
}
|
|
48
|
+
return keys;
|
|
49
|
+
},
|
|
50
|
+
get: (key) => storage.getItem(key),
|
|
51
|
+
set: (key, value) => storage.setItem(key, value),
|
|
52
|
+
remove: (key) => storage.removeItem(key),
|
|
53
|
+
clear: () => storage.clear(),
|
|
54
|
+
watch(onChange) {
|
|
55
|
+
patchStoragePrototype();
|
|
56
|
+
const bucket = listeners.get(storage) ?? new Set();
|
|
57
|
+
listeners.set(storage, bucket);
|
|
58
|
+
bucket.add(onChange);
|
|
59
|
+
// Writes from other tabs only show up as `storage` events.
|
|
60
|
+
const onStorageEvent = (event) => {
|
|
61
|
+
if (event.storageArea !== storage)
|
|
62
|
+
return;
|
|
63
|
+
if (event.key === null)
|
|
64
|
+
onChange('clear');
|
|
65
|
+
else
|
|
66
|
+
onChange(event.newValue === null ? 'remove' : 'set', event.key);
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener('storage', onStorageEvent);
|
|
69
|
+
return () => {
|
|
70
|
+
bucket.delete(onChange);
|
|
71
|
+
window.removeEventListener('storage', onStorageEvent);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function cookieStorage() {
|
|
77
|
+
const read = () => {
|
|
78
|
+
if (!document.cookie)
|
|
79
|
+
return [];
|
|
80
|
+
return document.cookie.split('; ').map((part) => {
|
|
81
|
+
const index = part.indexOf('=');
|
|
82
|
+
return index < 0 ? [decode(part), ''] : [decode(part.slice(0, index)), decode(part.slice(index + 1))];
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const remove = (key) => {
|
|
86
|
+
document.cookie = `${encodeURIComponent(key)}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
id: 'cookies',
|
|
90
|
+
label: 'Cookies',
|
|
91
|
+
note: 'HttpOnly cookies are invisible to JavaScript. Edits are written with path=/.',
|
|
92
|
+
keys: () => read().map(([key]) => key),
|
|
93
|
+
get: (key) => read().find(([k]) => k === key)?.[1] ?? null,
|
|
94
|
+
set: (key, value) => {
|
|
95
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; path=/`;
|
|
96
|
+
},
|
|
97
|
+
remove,
|
|
98
|
+
clear: () => {
|
|
99
|
+
for (const [key] of read())
|
|
100
|
+
remove(key);
|
|
101
|
+
},
|
|
102
|
+
watch: cookieChanges(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The Cookie Store API also reports document.cookie writes; without it, cookies refresh only on demand. */
|
|
106
|
+
function cookieChanges() {
|
|
107
|
+
const store = globalThis.cookieStore;
|
|
108
|
+
if (!store)
|
|
109
|
+
return undefined;
|
|
110
|
+
return (onChange) => {
|
|
111
|
+
const listener = (event) => {
|
|
112
|
+
const { changed = [], deleted = [] } = event;
|
|
113
|
+
for (const cookie of changed)
|
|
114
|
+
onChange('set', cookie.name);
|
|
115
|
+
for (const cookie of deleted)
|
|
116
|
+
onChange('remove', cookie.name);
|
|
117
|
+
};
|
|
118
|
+
store.addEventListener('change', listener);
|
|
119
|
+
return () => store.removeEventListener('change', listener);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function decode(text) {
|
|
123
|
+
try {
|
|
124
|
+
return decodeURIComponent(text);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return text;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAKA,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;AACpD,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,oFAAoF;AACpF,mEAAmE;AACnE,SAAS,qBAAqB;IAC5B,IAAI,gBAAgB,IAAI,OAAO,OAAO,KAAK,WAAW;QAAE,OAAO;IAC/D,gBAAgB,GAAG,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC;IAChC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAC7C,KAAK,CAAC,OAAO,GAAG,UAAyB,GAAW,EAAE,KAAa;QACjE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,CAAC,CAAC;IACF,KAAK,CAAC,UAAU,GAAG,UAAyB,GAAW;QACrD,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC3B,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC;IACF,KAAK,CAAC,KAAK,GAAG;QACZ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,OAAgB,EAAE,EAAa,EAAE,GAAY;IAC3D,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,KAAK,MAAM,QAAQ,IAAI,MAAM,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,mDAAmD;QACrD,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,EAAU,EAAE,KAAa;IACpE,OAAO;QACL,EAAE;QACF,KAAK;QACL,IAAI,EAAE,GAAG,EAAE;YACT,MAAM,IAAI,GAAa,EAAE,CAAC;YAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACxC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC3B,IAAI,GAAG,KAAK,IAAI;oBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;QAClC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC;QAChD,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACxC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QAC5B,KAAK,CAAC,QAAQ;YACZ,qBAAqB,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAY,CAAC;YAC7D,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC/B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACrB,2DAA2D;YAC3D,MAAM,cAAc,GAAG,CAAC,KAAmB,EAAE,EAAE;gBAC7C,IAAI,KAAK,CAAC,WAAW,KAAK,OAAO;oBAAE,OAAO;gBAC1C,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI;oBAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;;oBACrC,QAAQ,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;YACvE,CAAC,CAAC;YACF,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YACnD,OAAO,GAAG,EAAE;gBACV,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACxB,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YACxD,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,GAA4B,EAAE;QACzC,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC;QAChC,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAoB,EAAE;YAChE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAChC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACxG,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,EAAE;QAC7B,QAAQ,CAAC,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,CAAC,kDAAkD,CAAC;IACjG,CAAC,CAAC;IACF,OAAO;QACL,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,8EAA8E;QACpF,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;QACtC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;QAC1D,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAClB,QAAQ,CAAC,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,UAAU,CAAC;QACtF,CAAC;QACD,MAAM;QACN,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1C,CAAC;QACD,KAAK,EAAE,aAAa,EAAE;KACvB,CAAC;AACJ,CAAC;AAED,4GAA4G;AAC5G,SAAS,aAAa;IACpB,MAAM,KAAK,GAAI,UAA4C,CAAC,WAAW,CAAC;IACxE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,CAAC,QAAQ,EAAE,EAAE;QAClB,MAAM,QAAQ,GAAG,CAAC,KAAY,EAAE,EAAE;YAChC,MAAM,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,KAGtC,CAAC;YACF,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3D,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAChE,CAAC,CAAC;QACF,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC3C,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC7D,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-devtools/web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Browser SDK for Capybara: logs, network and storage for React, Vite and Next.js apps",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"capybara",
|
|
7
|
+
"devtools",
|
|
8
|
+
"debugger",
|
|
9
|
+
"logs",
|
|
10
|
+
"network",
|
|
11
|
+
"localstorage",
|
|
12
|
+
"react",
|
|
13
|
+
"vite",
|
|
14
|
+
"nextjs"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/lhphuc99/capybara#readme",
|
|
17
|
+
"bugs": "https://github.com/lhphuc99/capybara/issues",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/lhphuc99/capybara.git",
|
|
21
|
+
"directory": "packages/web"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@capybara-devtools/core": "0.2.0",
|
|
47
|
+
"@capybara-devtools/protocol": "0.2.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Plugin } from '@capybara-devtools/core';
|
|
2
|
+
|
|
3
|
+
export function windowErrorsPlugin(): Plugin {
|
|
4
|
+
return {
|
|
5
|
+
name: 'errors',
|
|
6
|
+
setup(ctx) {
|
|
7
|
+
// Bubbling listener only: resource load failures (broken <img>) don't bubble and stay out.
|
|
8
|
+
const onError = (event: ErrorEvent) => {
|
|
9
|
+
const args =
|
|
10
|
+
event.error !== undefined && event.error !== null
|
|
11
|
+
? [event.error]
|
|
12
|
+
: [event.message, `at ${event.filename}:${event.lineno}:${event.colno}`];
|
|
13
|
+
ctx.emit('log', { level: 'error', source: 'uncaught', args: args.map((arg) => ctx.serialize(arg)) });
|
|
14
|
+
};
|
|
15
|
+
const onRejection = (event: PromiseRejectionEvent) => {
|
|
16
|
+
ctx.emit('log', { level: 'error', source: 'unhandledrejection', args: [ctx.serialize(event.reason)] });
|
|
17
|
+
};
|
|
18
|
+
window.addEventListener('error', onError);
|
|
19
|
+
window.addEventListener('unhandledrejection', onRejection);
|
|
20
|
+
return () => {
|
|
21
|
+
window.removeEventListener('error', onError);
|
|
22
|
+
window.removeEventListener('unhandledrejection', onRejection);
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
consolePlugin,
|
|
3
|
+
createClient,
|
|
4
|
+
getActiveClient,
|
|
5
|
+
networkPlugin,
|
|
6
|
+
storagePlugin,
|
|
7
|
+
type CapybaraClient,
|
|
8
|
+
type NetworkPluginOptions,
|
|
9
|
+
type Plugin,
|
|
10
|
+
type StorageAdapter,
|
|
11
|
+
} from '@capybara-devtools/core';
|
|
12
|
+
import { CLIENT_PATH, DEFAULT_PORT } from '@capybara-devtools/protocol';
|
|
13
|
+
import { windowErrorsPlugin } from './errors.js';
|
|
14
|
+
import { cookieStorage, webStorage } from './storage.js';
|
|
15
|
+
|
|
16
|
+
export const SDK_VERSION = '0.2.0';
|
|
17
|
+
|
|
18
|
+
/** Traffic of a dev server's own tooling: Next.js error overlay requests and its HMR socket. */
|
|
19
|
+
const DEV_SERVER_NOISE = [/\/__nextjs_[\w-]+(\?|$)/, /\/_next\/webpack-hmr(\?|$)/];
|
|
20
|
+
/** React's development build prints this on every page when the React DevTools extension is missing. */
|
|
21
|
+
const CONSOLE_NOISE = [/Download the React DevTools for a better development experience/];
|
|
22
|
+
|
|
23
|
+
export interface InitOptions {
|
|
24
|
+
/** Shown in the session picker. Defaults to the page title. */
|
|
25
|
+
appName?: string;
|
|
26
|
+
/** Machine running `capybara`. Defaults to the hostname the page was loaded from. */
|
|
27
|
+
host?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
/** Pass false to skip setup, e.g. `enabled: import.meta.env.DEV`. */
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/** Capture console output and uncaught errors. Default true. */
|
|
32
|
+
logs?: boolean;
|
|
33
|
+
network?: boolean | NetworkPluginOptions;
|
|
34
|
+
/** `false` turns storage off; an array adds adapters next to localStorage, sessionStorage and cookies. */
|
|
35
|
+
storage?: boolean | StorageAdapter[];
|
|
36
|
+
plugins?: Plugin[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function init(options: InitOptions = {}): CapybaraClient | undefined {
|
|
40
|
+
// The window check makes this a no-op during server rendering, so the same entry works in Next.js.
|
|
41
|
+
if (options.enabled === false || typeof window === 'undefined') return undefined;
|
|
42
|
+
// Hot reload re-runs the entry file; patching again would record everything twice.
|
|
43
|
+
const existing = getActiveClient();
|
|
44
|
+
if (existing) return existing;
|
|
45
|
+
|
|
46
|
+
const plugins: Plugin[] = [];
|
|
47
|
+
if (options.logs !== false) plugins.push(consolePlugin({ ignore: CONSOLE_NOISE }), windowErrorsPlugin());
|
|
48
|
+
if (options.network !== false) {
|
|
49
|
+
const network = typeof options.network === 'object' ? options.network : {};
|
|
50
|
+
plugins.push(
|
|
51
|
+
networkPlugin({ ...network, ignoreUrls: [...DEV_SERVER_NOISE, ...metroSockets(), ...(network.ignoreUrls ?? [])] }),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (options.storage !== false) {
|
|
55
|
+
const extra = Array.isArray(options.storage) ? options.storage : [];
|
|
56
|
+
plugins.push(storagePlugin([...defaultStorages(), ...extra]));
|
|
57
|
+
}
|
|
58
|
+
plugins.push(...(options.plugins ?? []));
|
|
59
|
+
|
|
60
|
+
const host = options.host ?? (window.location.hostname || 'localhost');
|
|
61
|
+
return createClient({
|
|
62
|
+
url: `ws://${formatHost(host)}:${options.port ?? DEFAULT_PORT}${CLIENT_PATH}`,
|
|
63
|
+
session: {
|
|
64
|
+
appName: options.appName ?? (document.title || window.location.host || 'Web app'),
|
|
65
|
+
platform: 'web',
|
|
66
|
+
sdk: { name: '@capybara-devtools/web', version: SDK_VERSION },
|
|
67
|
+
url: window.location.href,
|
|
68
|
+
userAgent: navigator.userAgent,
|
|
69
|
+
},
|
|
70
|
+
plugins,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultStorages(): StorageAdapter[] {
|
|
75
|
+
const adapters: StorageAdapter[] = [];
|
|
76
|
+
// Reading these throws in sandboxed iframes and when site data is blocked.
|
|
77
|
+
try {
|
|
78
|
+
adapters.push(webStorage(window.localStorage, 'localStorage', 'Local Storage'));
|
|
79
|
+
} catch {
|
|
80
|
+
// unavailable
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
adapters.push(webStorage(window.sessionStorage, 'sessionStorage', 'Session Storage'));
|
|
84
|
+
} catch {
|
|
85
|
+
// unavailable
|
|
86
|
+
}
|
|
87
|
+
adapters.push(cookieStorage());
|
|
88
|
+
return adapters;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** With Expo web, Metro serves the page and also opens /hot and /message sockets on the same origin. */
|
|
92
|
+
function metroSockets(): RegExp[] {
|
|
93
|
+
const host = window.location.host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
return host ? [new RegExp(`^wss?://${host}/(?:hot|message)(?:[/?]|$)`)] : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatHost(host: string): string {
|
|
98
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { getActiveClient } from '@capybara-devtools/core';
|
|
102
|
+
export type { CapybaraClient, NetworkPluginOptions, Plugin, StorageAdapter } from '@capybara-devtools/core';
|
|
103
|
+
export { cookieStorage, webStorage } from './storage.js';
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { StorageAdapter } from '@capybara-devtools/core';
|
|
2
|
+
import type { StorageOp } from '@capybara-devtools/protocol';
|
|
3
|
+
|
|
4
|
+
type Listener = (op: StorageOp, key?: string) => void;
|
|
5
|
+
|
|
6
|
+
const listeners = new Map<Storage, Set<Listener>>();
|
|
7
|
+
let prototypePatched = false;
|
|
8
|
+
|
|
9
|
+
// Patched once and never undone: with no listeners it costs nothing, and undoing it
|
|
10
|
+
// could remove a wrapper another library installed on top of ours.
|
|
11
|
+
function patchStoragePrototype(): void {
|
|
12
|
+
if (prototypePatched || typeof Storage === 'undefined') return;
|
|
13
|
+
prototypePatched = true;
|
|
14
|
+
const proto = Storage.prototype;
|
|
15
|
+
const { setItem, removeItem, clear } = proto;
|
|
16
|
+
proto.setItem = function (this: Storage, key: string, value: string) {
|
|
17
|
+
setItem.call(this, key, value);
|
|
18
|
+
notify(this, 'set', String(key));
|
|
19
|
+
};
|
|
20
|
+
proto.removeItem = function (this: Storage, key: string) {
|
|
21
|
+
removeItem.call(this, key);
|
|
22
|
+
notify(this, 'remove', String(key));
|
|
23
|
+
};
|
|
24
|
+
proto.clear = function (this: Storage) {
|
|
25
|
+
clear.call(this);
|
|
26
|
+
notify(this, 'clear');
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function notify(storage: Storage, op: StorageOp, key?: string): void {
|
|
31
|
+
const bucket = listeners.get(storage);
|
|
32
|
+
if (!bucket) return;
|
|
33
|
+
for (const listener of bucket) {
|
|
34
|
+
try {
|
|
35
|
+
listener(op, key);
|
|
36
|
+
} catch {
|
|
37
|
+
// a broken listener must not break the app's write
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function webStorage(storage: Storage, id: string, label: string): StorageAdapter {
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
label,
|
|
46
|
+
keys: () => {
|
|
47
|
+
const keys: string[] = [];
|
|
48
|
+
for (let i = 0; i < storage.length; i++) {
|
|
49
|
+
const key = storage.key(i);
|
|
50
|
+
if (key !== null) keys.push(key);
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
},
|
|
54
|
+
get: (key) => storage.getItem(key),
|
|
55
|
+
set: (key, value) => storage.setItem(key, value),
|
|
56
|
+
remove: (key) => storage.removeItem(key),
|
|
57
|
+
clear: () => storage.clear(),
|
|
58
|
+
watch(onChange) {
|
|
59
|
+
patchStoragePrototype();
|
|
60
|
+
const bucket = listeners.get(storage) ?? new Set<Listener>();
|
|
61
|
+
listeners.set(storage, bucket);
|
|
62
|
+
bucket.add(onChange);
|
|
63
|
+
// Writes from other tabs only show up as `storage` events.
|
|
64
|
+
const onStorageEvent = (event: StorageEvent) => {
|
|
65
|
+
if (event.storageArea !== storage) return;
|
|
66
|
+
if (event.key === null) onChange('clear');
|
|
67
|
+
else onChange(event.newValue === null ? 'remove' : 'set', event.key);
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener('storage', onStorageEvent);
|
|
70
|
+
return () => {
|
|
71
|
+
bucket.delete(onChange);
|
|
72
|
+
window.removeEventListener('storage', onStorageEvent);
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cookieStorage(): StorageAdapter {
|
|
79
|
+
const read = (): Array<[string, string]> => {
|
|
80
|
+
if (!document.cookie) return [];
|
|
81
|
+
return document.cookie.split('; ').map((part): [string, string] => {
|
|
82
|
+
const index = part.indexOf('=');
|
|
83
|
+
return index < 0 ? [decode(part), ''] : [decode(part.slice(0, index)), decode(part.slice(index + 1))];
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const remove = (key: string) => {
|
|
87
|
+
document.cookie = `${encodeURIComponent(key)}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
id: 'cookies',
|
|
91
|
+
label: 'Cookies',
|
|
92
|
+
note: 'HttpOnly cookies are invisible to JavaScript. Edits are written with path=/.',
|
|
93
|
+
keys: () => read().map(([key]) => key),
|
|
94
|
+
get: (key) => read().find(([k]) => k === key)?.[1] ?? null,
|
|
95
|
+
set: (key, value) => {
|
|
96
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; path=/`;
|
|
97
|
+
},
|
|
98
|
+
remove,
|
|
99
|
+
clear: () => {
|
|
100
|
+
for (const [key] of read()) remove(key);
|
|
101
|
+
},
|
|
102
|
+
watch: cookieChanges(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The Cookie Store API also reports document.cookie writes; without it, cookies refresh only on demand. */
|
|
107
|
+
function cookieChanges(): StorageAdapter['watch'] {
|
|
108
|
+
const store = (globalThis as { cookieStore?: EventTarget }).cookieStore;
|
|
109
|
+
if (!store) return undefined;
|
|
110
|
+
return (onChange) => {
|
|
111
|
+
const listener = (event: Event) => {
|
|
112
|
+
const { changed = [], deleted = [] } = event as Event & {
|
|
113
|
+
changed?: Array<{ name?: string }>;
|
|
114
|
+
deleted?: Array<{ name?: string }>;
|
|
115
|
+
};
|
|
116
|
+
for (const cookie of changed) onChange('set', cookie.name);
|
|
117
|
+
for (const cookie of deleted) onChange('remove', cookie.name);
|
|
118
|
+
};
|
|
119
|
+
store.addEventListener('change', listener);
|
|
120
|
+
return () => store.removeEventListener('change', listener);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function decode(text: string): string {
|
|
125
|
+
try {
|
|
126
|
+
return decodeURIComponent(text);
|
|
127
|
+
} catch {
|
|
128
|
+
return text;
|
|
129
|
+
}
|
|
130
|
+
}
|