@mindmakr/reins 0.0.0-stage → 0.1.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 +6 -2
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +24 -0
- package/dist/bridge/args.d.ts +15 -0
- package/dist/bridge/args.js +59 -0
- package/dist/bridge/audit.d.ts +13 -0
- package/dist/bridge/audit.js +13 -0
- package/dist/bridge/clipboard.d.ts +2 -0
- package/dist/bridge/clipboard.js +31 -0
- package/dist/bridge/hub.d.ts +56 -0
- package/dist/bridge/hub.js +162 -0
- package/dist/bridge/index.d.ts +11 -0
- package/dist/bridge/index.js +11 -0
- package/dist/bridge/mcp.d.ts +5 -0
- package/dist/bridge/mcp.js +22 -0
- package/dist/bridge/origin.d.ts +7 -0
- package/dist/bridge/origin.js +23 -0
- package/dist/bridge/report-cli.d.ts +13 -0
- package/dist/bridge/report-cli.js +82 -0
- package/dist/bridge/session.d.ts +24 -0
- package/dist/bridge/session.js +116 -0
- package/dist/bridge/snap.d.ts +28 -0
- package/dist/bridge/snap.js +65 -0
- package/dist/bridge/token.d.ts +10 -0
- package/dist/bridge/token.js +35 -0
- package/dist/bridge/tools.d.ts +18 -0
- package/dist/bridge/tools.js +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/dom.d.ts +2 -0
- package/dist/dom.js +48 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +6 -0
- package/dist/expo.d.ts +27 -0
- package/dist/expo.js +48 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +267 -0
- package/dist/metro.d.ts +16 -0
- package/dist/metro.js +45 -0
- package/dist/network.d.ts +14 -0
- package/dist/network.js +50 -0
- package/dist/next-token-route.d.ts +9 -0
- package/dist/next-token-route.js +17 -0
- package/dist/next.d.ts +13 -0
- package/dist/next.js +27 -0
- package/dist/node.d.ts +80 -0
- package/dist/node.js +414 -0
- package/dist/react.d.ts +281 -0
- package/dist/react.js +33 -0
- package/dist/record-stub.d.ts +18 -0
- package/dist/record-stub.js +6 -0
- package/dist/record.d.ts +28 -0
- package/dist/record.js +104 -0
- package/dist/redact.d.ts +7 -0
- package/dist/redact.js +30 -0
- package/dist/report.d.ts +121 -0
- package/dist/report.js +353 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +27 -0
- package/dist/stub.d.ts +88 -0
- package/dist/stub.js +38 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +1 -0
- package/dist/verify-cli.d.ts +2 -0
- package/dist/verify-cli.js +3 -0
- package/dist/verify.d.ts +17 -0
- package/dist/verify.js +60 -0
- package/dist/vite.d.ts +32 -0
- package/dist/vite.js +33 -0
- package/dist/webmcp.d.ts +3 -0
- package/dist/webmcp.js +20 -0
- package/package.json +59 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { devEnabled } from './env.js';
|
|
2
|
+
import { redactDeep } from './redact.js';
|
|
3
|
+
import type { Action, ActionInfo, Box, ConnectOptions, DomNode, DomOptions, ErrEntry, Measure, NetEntry, PerfReport } from './types.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export { devEnabled, redactDeep };
|
|
6
|
+
export declare const DEV_MARKER = "__REINS_DEV_ONLY__";
|
|
7
|
+
export declare const VERSION = "0.1.0";
|
|
8
|
+
export interface ReinsHandle {
|
|
9
|
+
version: string;
|
|
10
|
+
actions(): ActionInfo[];
|
|
11
|
+
run(name: string, input?: unknown, opts?: {
|
|
12
|
+
confirm?: boolean;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
state(name?: string): Record<string, unknown>;
|
|
15
|
+
network(limit?: number): NetEntry[];
|
|
16
|
+
errors(limit?: number): ErrEntry[];
|
|
17
|
+
panel(id: string): Promise<Box | null>;
|
|
18
|
+
perf(): PerfReport;
|
|
19
|
+
dom(opts?: DomOptions): Promise<DomNode | null>;
|
|
20
|
+
}
|
|
21
|
+
export type Reins = ReturnType<typeof createReins>;
|
|
22
|
+
export declare function createReins(g?: any): {
|
|
23
|
+
run: (n: string, i?: unknown, o?: {
|
|
24
|
+
confirm?: boolean;
|
|
25
|
+
}) => Promise<unknown>;
|
|
26
|
+
actions: () => ActionInfo[];
|
|
27
|
+
network: (limit?: number) => NetEntry[];
|
|
28
|
+
errors: (limit?: number) => ErrEntry[];
|
|
29
|
+
perf: () => PerfReport;
|
|
30
|
+
dom: (o?: DomOptions) => Promise<DomNode | null>;
|
|
31
|
+
panel: (id: string, o?: {
|
|
32
|
+
scroll?: boolean;
|
|
33
|
+
}) => Promise<Box | null>;
|
|
34
|
+
marker: string;
|
|
35
|
+
action: <I = any, O = any>(def: Action<I, O>) => () => void;
|
|
36
|
+
state: (name: string, get: () => unknown) => () => void;
|
|
37
|
+
registerPanel: (id: string, measure: Measure) => () => void;
|
|
38
|
+
markComponent: (id: string, phase?: "mount" | "render") => void;
|
|
39
|
+
panels: () => string[];
|
|
40
|
+
connect: (o?: ConnectOptions) => Promise<boolean>;
|
|
41
|
+
disconnect: () => void;
|
|
42
|
+
isPaired: () => boolean;
|
|
43
|
+
record: (o?: ConnectOptions | false) => boolean;
|
|
44
|
+
snapshot: (name?: string) => Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
export declare const reins: {
|
|
47
|
+
run: (n: string, i?: unknown, o?: {
|
|
48
|
+
confirm?: boolean;
|
|
49
|
+
}) => Promise<unknown>;
|
|
50
|
+
actions: () => ActionInfo[];
|
|
51
|
+
network: (limit?: number) => NetEntry[];
|
|
52
|
+
errors: (limit?: number) => ErrEntry[];
|
|
53
|
+
perf: () => PerfReport;
|
|
54
|
+
dom: (o?: DomOptions) => Promise<DomNode | null>;
|
|
55
|
+
panel: (id: string, o?: {
|
|
56
|
+
scroll?: boolean;
|
|
57
|
+
}) => Promise<Box | null>;
|
|
58
|
+
marker: string;
|
|
59
|
+
action: <I = any, O = any>(def: Action<I, O>) => () => void;
|
|
60
|
+
state: (name: string, get: () => unknown) => () => void;
|
|
61
|
+
registerPanel: (id: string, measure: Measure) => () => void;
|
|
62
|
+
markComponent: (id: string, phase?: "mount" | "render") => void;
|
|
63
|
+
panels: () => string[];
|
|
64
|
+
connect: (o?: ConnectOptions) => Promise<boolean>;
|
|
65
|
+
disconnect: () => void;
|
|
66
|
+
isPaired: () => boolean;
|
|
67
|
+
record: (o?: ConnectOptions | false) => boolean;
|
|
68
|
+
snapshot: (name?: string) => Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
export default reins;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { devEnabled } from './env.js';
|
|
2
|
+
import { msg, patchNetwork, R, ring, watchErrors } from './network.js';
|
|
3
|
+
import { redactDeep, redactPairs, redactUrl } from './redact.js';
|
|
4
|
+
import { findModelContext, registerTool } from './webmcp.js';
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export { devEnabled, redactDeep };
|
|
7
|
+
export const DEV_MARKER = '__REINS_DEV_ONLY__';
|
|
8
|
+
export const VERSION = '0.1.0';
|
|
9
|
+
const NAME = /^[\w][\w.-]{0,63}$/;
|
|
10
|
+
const fail = (s) => {
|
|
11
|
+
throw new Error('reins: ' + s);
|
|
12
|
+
};
|
|
13
|
+
export function createReins(g = globalThis) {
|
|
14
|
+
const M = () => new Map(), actions = M(), states = M(), measures = M(), comps = M();
|
|
15
|
+
let opts = {}, net = ring(100), errs = ring(100), sock = null, paired = false, rec = false, undo = [], mc = null, connecting = null, retry = null, delay = 2000;
|
|
16
|
+
const nowMs = () => R(g.performance?.now?.()), findPanel = (id) => g.document?.querySelector?.(`[data-reins="${id.replace(/["\\]/g, '\\$&')}"]`), boxOf = (r) => ({ x: R(r.left), y: R(r.top), width: R(r.width), height: R(r.height) }), locUrl = () => g.location?.href && redactUrl('' + g.location.href, opts.allow?.queryKeys), send = (m) => sock?.send(JSON.stringify(m)), keep = (map, k, v) => (map.set(k, v), () => void (map.get(k) === v && (v._u?.(), map.delete(k)))), addTool = (a) => {
|
|
17
|
+
a._u?.();
|
|
18
|
+
try {
|
|
19
|
+
undo.push((a._u = registerTool(mc, a, (n, i, c) => runAction(n, i, c, 'webmcp'))));
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
};
|
|
23
|
+
function action(def) {
|
|
24
|
+
if (!NAME.test(def?.name))
|
|
25
|
+
fail('bad action name');
|
|
26
|
+
if (typeof def.run != 'function')
|
|
27
|
+
fail('no run function');
|
|
28
|
+
if (paired && mc)
|
|
29
|
+
addTool(def);
|
|
30
|
+
return keep(actions, def.name, def);
|
|
31
|
+
}
|
|
32
|
+
const state = (name, get) => keep(states, name, get);
|
|
33
|
+
const registerPanel = (id, measure) => keep(measures, id, measure);
|
|
34
|
+
function markComponent(id, phase) {
|
|
35
|
+
if (!id)
|
|
36
|
+
return;
|
|
37
|
+
const t = nowMs(), c = comps.get(id);
|
|
38
|
+
if (!c || phase == 'mount')
|
|
39
|
+
comps.set(id, { id, mountedAtMs: t, lastRenderMs: t, renders: (c?.renders || 0) + 1 });
|
|
40
|
+
else
|
|
41
|
+
(c.lastRenderMs = t), c.renders++;
|
|
42
|
+
}
|
|
43
|
+
const list = () => [...actions.values()].map(({ name, description, input = { type: 'object', properties: {} }, consequential, readOnly }) => ({
|
|
44
|
+
name,
|
|
45
|
+
description,
|
|
46
|
+
input,
|
|
47
|
+
consequential: !!consequential,
|
|
48
|
+
readOnly: !!readOnly,
|
|
49
|
+
}));
|
|
50
|
+
const mustBePaired = () => !undo[0] && fail('not paired');
|
|
51
|
+
async function runAction(name, input, confirm, via) {
|
|
52
|
+
mustBePaired();
|
|
53
|
+
const a = actions.get(name), p = g.performance, mark = 'reins:' + name;
|
|
54
|
+
let error;
|
|
55
|
+
try {
|
|
56
|
+
if (!a)
|
|
57
|
+
fail('unknown action');
|
|
58
|
+
if (a.consequential && !confirm)
|
|
59
|
+
fail('confirm: true required');
|
|
60
|
+
p?.mark?.(mark);
|
|
61
|
+
return await a.run(input ?? {});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
error = msg(e);
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (via != 'bridge')
|
|
69
|
+
send({ type: 'audit', entry: { method: 'run', name, via, ok: !error, error, at: Date.now() } });
|
|
70
|
+
try {
|
|
71
|
+
p?.measure?.('[reins] ' + name, mark);
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function snapshot(name) {
|
|
77
|
+
mustBePaired();
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const k of name ? [name] : states.keys()) {
|
|
80
|
+
const get = states.get(k) || fail('unknown state');
|
|
81
|
+
try {
|
|
82
|
+
out[k] = redactDeep(get(), opts.allow?.stateKeys);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
out[k] = { error: msg(e) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function panels() {
|
|
91
|
+
const ids = new Set(measures.keys());
|
|
92
|
+
g.document?.querySelectorAll?.('[data-reins]').forEach((el) => ids.add(el.getAttribute('data-reins')));
|
|
93
|
+
return [...ids];
|
|
94
|
+
}
|
|
95
|
+
async function panel(id, o = {}) {
|
|
96
|
+
const m = measures.get(id);
|
|
97
|
+
if (m)
|
|
98
|
+
return m();
|
|
99
|
+
const el = findPanel(id);
|
|
100
|
+
if (!el)
|
|
101
|
+
return null;
|
|
102
|
+
if (o.scroll)
|
|
103
|
+
el.scrollIntoView?.({ block: 'center' });
|
|
104
|
+
return {
|
|
105
|
+
id,
|
|
106
|
+
...boxOf(el.getBoundingClientRect()),
|
|
107
|
+
scrollX: g.scrollX || 0,
|
|
108
|
+
scrollY: g.scrollY || 0,
|
|
109
|
+
devicePixelRatio: g.devicePixelRatio || 1,
|
|
110
|
+
viewportWidth: g.innerWidth,
|
|
111
|
+
viewportHeight: g.innerHeight,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function perf() {
|
|
115
|
+
const p = g.performance, ent = (t) => p?.getEntriesByType?.(t) || [], nav = ent('navigation')[0], paints = ent('paint'), num = (v) => (v >= 0 && v !== null ? R(v) : null), pt = (n) => num(paints.find((e) => e.name == 'first-' + n)?.startTime), lifecycle = {
|
|
116
|
+
ttfbMs: num(nav?.responseStart),
|
|
117
|
+
domInteractiveMs: num(nav?.domInteractive),
|
|
118
|
+
domContentLoadedMs: num(nav?.domContentLoadedEventEnd),
|
|
119
|
+
loadMs: num(nav?.loadEventEnd),
|
|
120
|
+
firstPaintMs: pt('paint'),
|
|
121
|
+
firstContentfulPaintMs: pt('contentful-paint'),
|
|
122
|
+
domNodes: g.document?.getElementsByTagName?.('*')?.length || 0,
|
|
123
|
+
nowMs: nowMs(),
|
|
124
|
+
}, components = [...comps.values()].sort((a, b) => a.mountedAtMs - b.mountedAtMs),
|
|
125
|
+
// Full (redacted) entries; the bridge trims them to the timing fields.
|
|
126
|
+
calls = net.list(), act = ent('measure').flatMap((m) => (m.name?.startsWith?.('[reins] ') ? [{ name: m.name, startMs: R(m.startTime), durationMs: R(m.duration) }] : []));
|
|
127
|
+
let sum = 0, failedCalls = 0, slowest = null;
|
|
128
|
+
for (const c of calls) {
|
|
129
|
+
sum += c.ms;
|
|
130
|
+
if (!c.status || c.status >= 400)
|
|
131
|
+
failedCalls++;
|
|
132
|
+
if (!slowest || c.ms > slowest.ms)
|
|
133
|
+
slowest = c;
|
|
134
|
+
}
|
|
135
|
+
// The timeline is built from these parts by `withTimeline` in `@mindmakr/reins/report`, in the bridge and in record mode.
|
|
136
|
+
return {
|
|
137
|
+
lifecycle,
|
|
138
|
+
components,
|
|
139
|
+
api: { totalCalls: calls.length, failedCalls, avgMs: R(calls.length && sum / calls.length), maxMs: slowest?.ms || 0, slowest, calls },
|
|
140
|
+
actions: act,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// The inspector is a file of its own, loaded on the first call: a page that is never inspected never loads it.
|
|
144
|
+
const dom = (o = {}) => import('./dom.js').then((m) => m.inspect(g, o));
|
|
145
|
+
const network = (limit) => net.list(limit), errors = (limit) => errs.list(limit), runner = (via) => (n, i, o) => runAction(n, i, !!o?.confirm, via), methods = {
|
|
146
|
+
list: () => ({ actions: list(), states: [...states.keys()], panels: panels() }),
|
|
147
|
+
run: (p) => runAction(p.name, p.input, p.confirm === true, 'bridge'),
|
|
148
|
+
state: (p) => snapshot(p.name),
|
|
149
|
+
network: (p) => network(p.limit).filter((e) => !p.url || e.url.includes(p.url)),
|
|
150
|
+
errors: (p) => errors(p.limit),
|
|
151
|
+
panel: (p) => panel(p.id, p),
|
|
152
|
+
perf,
|
|
153
|
+
dom,
|
|
154
|
+
}, base = { actions: list, network, errors, perf, dom, panel }, handleForHumans = Object.freeze({ version: VERSION, ...base, run: runner('devtools'), state: snapshot });
|
|
155
|
+
// `undo` starts with the network and error patches, so it is non-empty exactly while reins is live (paired or recording).
|
|
156
|
+
function start() {
|
|
157
|
+
if (undo[0])
|
|
158
|
+
return;
|
|
159
|
+
const allow = opts.allow || {}, sz = opts.bufferSize || 100;
|
|
160
|
+
net = ring(sz);
|
|
161
|
+
errs = ring(sz);
|
|
162
|
+
undo = [patchNetwork(g, net.push, allow, () => paired), watchErrors(g, errs.push)];
|
|
163
|
+
if (g.location?.href)
|
|
164
|
+
states.set('location', locUrl);
|
|
165
|
+
for (const k of ['localStorage', 'sessionStorage'])
|
|
166
|
+
if (g[k])
|
|
167
|
+
states.set(k, () => redactPairs(Object.entries(g[k]), allow.storageKeys));
|
|
168
|
+
if (g.document?.cookie != null)
|
|
169
|
+
states.set('cookies', () => redactPairs(g.document.cookie.split(/; */).filter(Boolean).map((c) => c.split('=')), allow.cookies));
|
|
170
|
+
}
|
|
171
|
+
function pair(s) {
|
|
172
|
+
sock = s;
|
|
173
|
+
paired = true;
|
|
174
|
+
delay = 2000;
|
|
175
|
+
start();
|
|
176
|
+
undo.push(() => delete g.__reins__);
|
|
177
|
+
mc = opts.webmcp !== false && findModelContext(g);
|
|
178
|
+
if (mc)
|
|
179
|
+
actions.forEach(addTool);
|
|
180
|
+
try {
|
|
181
|
+
g.__reins__ = handleForHumans;
|
|
182
|
+
}
|
|
183
|
+
catch { }
|
|
184
|
+
}
|
|
185
|
+
// Undoes everything after the first `n` entries: 2 keeps the patches running for record mode.
|
|
186
|
+
const off = (n) => undo.splice(n).forEach((f) => f());
|
|
187
|
+
function unpair() {
|
|
188
|
+
paired = false;
|
|
189
|
+
sock = mc = null;
|
|
190
|
+
off(rec ? 2 : 0);
|
|
191
|
+
}
|
|
192
|
+
/** Record mode (`@mindmakr/reins/record`): starts the buffers without a bridge, in dev builds only. `false` stops it. */
|
|
193
|
+
const record = (o) => ((rec = o !== false && devEnabled()), paired || (rec ? ((opts = o || opts), start()) : off(0)), rec);
|
|
194
|
+
async function open() {
|
|
195
|
+
if (g.document && opts.lazy !== false)
|
|
196
|
+
await new Promise((r) => (g.document.readyState == 'complete' ? setTimeout(r) : g.addEventListener('load', r, { once: true })));
|
|
197
|
+
let token = opts.token;
|
|
198
|
+
if (!token && g.fetch) {
|
|
199
|
+
const res = await g.fetch(opts.tokenUrl || '/__reins/token', { cache: 'no-store' });
|
|
200
|
+
token = res.ok && (await res.text()).trim();
|
|
201
|
+
}
|
|
202
|
+
if (!token)
|
|
203
|
+
fail('no token');
|
|
204
|
+
return new Promise((res, rej) => {
|
|
205
|
+
const s = new (opts.WebSocket || g.WebSocket)(opts.url || 'ws://127.0.0.1:7357');
|
|
206
|
+
let done = false;
|
|
207
|
+
s.onopen = () => s.send(JSON.stringify({ type: 'hello', token, marker: DEV_MARKER, version: VERSION, url: locUrl(), title: g.document?.title, meta: opts.meta }));
|
|
208
|
+
s.onmessage = async (ev) => {
|
|
209
|
+
try {
|
|
210
|
+
const m = JSON.parse('' + ev.data);
|
|
211
|
+
if (!done) {
|
|
212
|
+
done = true;
|
|
213
|
+
return m.type == 'welcome' ? (pair(s), res(true)) : (s.close(), rej());
|
|
214
|
+
}
|
|
215
|
+
if (m.type == 'call')
|
|
216
|
+
try {
|
|
217
|
+
send({ type: 'result', id: m.id, ok: true, result: (await (methods[m.method] || fail('bad method'))(m.params || {})) ?? null });
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
send({ type: 'result', id: m.id, ok: false, error: msg(e) });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
};
|
|
225
|
+
s.onclose = () => (!done ? rej() : sock == s && (unpair(), schedule()));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function schedule() {
|
|
229
|
+
connecting = null;
|
|
230
|
+
if (opts.reconnect !== false && !retry)
|
|
231
|
+
retry = setTimeout(() => ((retry = null), connect(opts)), delay < 30000 ? (delay *= 2) / 2 : delay);
|
|
232
|
+
}
|
|
233
|
+
function connect(o = {}) {
|
|
234
|
+
if (!devEnabled())
|
|
235
|
+
return Promise.resolve(false);
|
|
236
|
+
if (!connecting) {
|
|
237
|
+
opts = o;
|
|
238
|
+
connecting = open().catch(() => (schedule(), false));
|
|
239
|
+
}
|
|
240
|
+
return connecting;
|
|
241
|
+
}
|
|
242
|
+
function disconnect() {
|
|
243
|
+
opts = { reconnect: false };
|
|
244
|
+
clearTimeout(retry);
|
|
245
|
+
retry = connecting = null;
|
|
246
|
+
const s = sock;
|
|
247
|
+
unpair();
|
|
248
|
+
s?.close();
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
marker: DEV_MARKER,
|
|
252
|
+
action,
|
|
253
|
+
state,
|
|
254
|
+
registerPanel,
|
|
255
|
+
markComponent,
|
|
256
|
+
panels,
|
|
257
|
+
connect,
|
|
258
|
+
disconnect,
|
|
259
|
+
isPaired: () => paired,
|
|
260
|
+
record,
|
|
261
|
+
snapshot,
|
|
262
|
+
...base,
|
|
263
|
+
run: runner('api'),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
export const reins = createReins();
|
|
267
|
+
export default reins;
|
package/dist/metro.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type MetroConfig = Record<string, any>;
|
|
2
|
+
export interface ReinsMetroOptions {
|
|
3
|
+
/** Project folder holding `.reins/`. Default: current folder. */
|
|
4
|
+
projectRoot?: string;
|
|
5
|
+
/** Keep reins in production bundles. Prefer REINS_ENABLE=1, and only for staging. */
|
|
6
|
+
keep?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Whether this Metro run should swap reins for its stub. `expo export` and release builds set NODE_ENV=production. */
|
|
9
|
+
export declare function shouldStub(options?: ReinsMetroOptions, env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a Metro config:
|
|
12
|
+
* - dev: serves the pairing token at /__reins/token on the Metro server;
|
|
13
|
+
* - production: resolves `@mindmakr/reins` to `@mindmakr/reins/stub` and `@mindmakr/reins/record` to `@mindmakr/reins/record-stub`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withReins(config: MetroConfig, options?: ReinsMetroOptions): MetroConfig;
|
|
16
|
+
export default withReins;
|
package/dist/metro.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Metro config helper for reins. Runs in Node, never in the app.
|
|
2
|
+
import { keepInBuild, TOKEN_ROUTE, tokenResponse } from '@mindmakr/reins/server';
|
|
3
|
+
/** Whether this Metro run should swap reins for its stub. `expo export` and release builds set NODE_ENV=production. */
|
|
4
|
+
export function shouldStub(options = {}, env = process.env) {
|
|
5
|
+
return env.NODE_ENV === 'production' && !options.keep && !keepInBuild(env);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Wraps a Metro config:
|
|
9
|
+
* - dev: serves the pairing token at /__reins/token on the Metro server;
|
|
10
|
+
* - production: resolves `@mindmakr/reins` to `@mindmakr/reins/stub` and `@mindmakr/reins/record` to `@mindmakr/reins/record-stub`.
|
|
11
|
+
*/
|
|
12
|
+
export function withReins(config, options = {}) {
|
|
13
|
+
const root = options.projectRoot || process.cwd();
|
|
14
|
+
const server = config.server || {};
|
|
15
|
+
const resolver = config.resolver || {};
|
|
16
|
+
const prevEnhance = server.enhanceMiddleware;
|
|
17
|
+
const prevResolve = resolver.resolveRequest;
|
|
18
|
+
const stub = shouldStub(options);
|
|
19
|
+
return {
|
|
20
|
+
...config,
|
|
21
|
+
server: {
|
|
22
|
+
...server,
|
|
23
|
+
enhanceMiddleware(middleware, metroServer) {
|
|
24
|
+
const inner = prevEnhance ? prevEnhance(middleware, metroServer) : middleware;
|
|
25
|
+
return (req, res, next) => {
|
|
26
|
+
if (req.url !== TOKEN_ROUTE)
|
|
27
|
+
return inner(req, res, next);
|
|
28
|
+
const { status, body } = tokenResponse(root, req.headers);
|
|
29
|
+
res.statusCode = status;
|
|
30
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
31
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
32
|
+
res.end(body);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
resolver: {
|
|
37
|
+
...resolver,
|
|
38
|
+
resolveRequest(context, moduleName, platform) {
|
|
39
|
+
const name = stub && moduleName === '@mindmakr/reins' ? '@mindmakr/reins/stub' : stub && moduleName === '@mindmakr/reins/record' ? '@mindmakr/reins/record-stub' : moduleName;
|
|
40
|
+
return prevResolve ? prevResolve(context, name, platform) : context.resolveRequest(context, name, platform);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export default withReins;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Allow, ErrEntry, NetEntry } from './types.js';
|
|
2
|
+
export interface Ring<T> {
|
|
3
|
+
push(item: T): void;
|
|
4
|
+
list(limit?: number): T[];
|
|
5
|
+
}
|
|
6
|
+
export declare const R: (n: any) => number;
|
|
7
|
+
export declare const msg: (v: any) => string;
|
|
8
|
+
export declare function ring<T>(size: number): Ring<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Wraps `fetch` to record redacted calls. While `trace()` is true, same-origin calls also carry an
|
|
11
|
+
* `x-reins-trace` id, so `@mindmakr/reins/node` can join the server half. Cross-origin calls never get it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function patchNetwork(g: any, record: (e: NetEntry) => void, { queryKeys, headers, bodyKeys }: Allow, trace: () => any): () => void;
|
|
14
|
+
export declare function watchErrors(g: any, record: (e: ErrEntry) => void): () => void;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { REDACTED, redactBody, redactHeaders, redactUrl } from './redact.js';
|
|
2
|
+
export const R = (n) => Math.round(n || 0);
|
|
3
|
+
export const msg = (v) => {
|
|
4
|
+
try {
|
|
5
|
+
return '' + (v?.message || (typeof v == 'object' ? JSON.stringify(v) : v));
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return '' + v;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export function ring(size) {
|
|
12
|
+
const a = [];
|
|
13
|
+
return {
|
|
14
|
+
push: (x) => void (a.push(x) > size && a.shift()),
|
|
15
|
+
list: (n) => a.slice(n ? -n : 0),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps `fetch` to record redacted calls. While `trace()` is true, same-origin calls also carry an
|
|
20
|
+
* `x-reins-trace` id, so `@mindmakr/reins/node` can join the server half. Cross-origin calls never get it.
|
|
21
|
+
*/
|
|
22
|
+
export function patchNetwork(g, record, { queryKeys, headers, bodyKeys }, trace) {
|
|
23
|
+
const now = () => R(g.performance?.now?.() ?? Date.now()), f = g.fetch, tid = Math.random().toString(36).slice(2, 8), hdr = (h, o = {}) => (h?.forEach ? h.forEach((v, k) => (o[k] = v)) : Object.assign(o, h), redactHeaders(o, headers));
|
|
24
|
+
let n = 0;
|
|
25
|
+
if (!f)
|
|
26
|
+
return () => { };
|
|
27
|
+
g.fetch = function (u, o = {}) {
|
|
28
|
+
const startMs = now(), req = u?.url ? u : {}, b = o.body, e = {
|
|
29
|
+
method: (o.method || req.method || 'GET').toUpperCase(),
|
|
30
|
+
url: redactUrl('' + (req.url || u), queryKeys),
|
|
31
|
+
startMs,
|
|
32
|
+
at: Date.now(),
|
|
33
|
+
requestHeaders: hdr(o.headers || req.headers),
|
|
34
|
+
requestBody: b == null ? undefined : typeof b == 'string' ? redactBody(b, bodyKeys) : REDACTED,
|
|
35
|
+
}, fin = (status, h) => ((e.status = status), (e.ms = now() - startMs), (e.responseHeaders = hdr(h)), record(e));
|
|
36
|
+
try {
|
|
37
|
+
if (trace() && new URL(req.url || u, g.location.href).origin == g.location.origin)
|
|
38
|
+
(o = { ...o, headers: new g.Headers(o.headers || req.headers) }).headers.set('x-reins-trace', (e.traceId = tid + '-' + ++n));
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
return f.call(this, u, o).then((r) => (fin(r.status, r.headers), bodyKeys?.length && r.clone?.().text().then((t) => (e.responseBody = redactBody(t, bodyKeys)), () => { }), r), (err) => ((e.error = msg(err)), fin(0), Promise.reject(err)));
|
|
42
|
+
};
|
|
43
|
+
return () => (g.fetch = f);
|
|
44
|
+
}
|
|
45
|
+
export function watchErrors(g, record) {
|
|
46
|
+
const add = (kind, v) => record({ kind, message: msg(v).slice(0, 2000), stack: v?.stack, at: Date.now() }), c = g.console, orig = c?.error, ev = (m, fn) => (g.addEventListener?.(m, fn), () => g.removeEventListener?.(m, fn)), u1 = ev('error', (e) => add('error', e.error || e.message)), u2 = ev('unhandledrejection', (e) => add('rejection', e.reason));
|
|
47
|
+
if (orig)
|
|
48
|
+
c.error = (...a) => (add('console', a.map(msg).join(' ')), orig.apply(c, a));
|
|
49
|
+
return () => ((orig && (c.error = orig)), u1(), u2());
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Router handler that serves the pairing token in development.
|
|
3
|
+
* Add `app/api/reins-token/route.ts` with:
|
|
4
|
+
* export { GET } from '@mindmakr/reins/next/token-route';
|
|
5
|
+
* export const dynamic = 'force-dynamic';
|
|
6
|
+
* and connect with `reins.connect({ tokenUrl: '/api/reins-token' })`.
|
|
7
|
+
* In production it always answers 404.
|
|
8
|
+
*/
|
|
9
|
+
export declare function GET(request: Request): Promise<Response>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { tokenResponse } from '@mindmakr/reins/server';
|
|
2
|
+
/**
|
|
3
|
+
* App Router handler that serves the pairing token in development.
|
|
4
|
+
* Add `app/api/reins-token/route.ts` with:
|
|
5
|
+
* export { GET } from '@mindmakr/reins/next/token-route';
|
|
6
|
+
* export const dynamic = 'force-dynamic';
|
|
7
|
+
* and connect with `reins.connect({ tokenUrl: '/api/reins-token' })`.
|
|
8
|
+
* In production it always answers 404.
|
|
9
|
+
*/
|
|
10
|
+
export async function GET(request) {
|
|
11
|
+
if (process.env.NODE_ENV === 'production')
|
|
12
|
+
return new Response('Not found', { status: 404 });
|
|
13
|
+
const headers = {};
|
|
14
|
+
request.headers.forEach((v, k) => (headers[k] = v));
|
|
15
|
+
const { status, body } = tokenResponse(process.cwd(), headers);
|
|
16
|
+
return new Response(body, { status, headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' } });
|
|
17
|
+
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type NextConfig = Record<string, any>;
|
|
2
|
+
export interface ReinsNextOptions {
|
|
3
|
+
/** Keep reins in production builds. Prefer REINS_ENABLE=1, and only for staging. */
|
|
4
|
+
keep?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Whether a build should swap reins for its stub. */
|
|
7
|
+
export declare function shouldStub(dev: boolean, options?: ReinsNextOptions, env?: NodeJS.ProcessEnv): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Wraps a Next.js config. Production builds resolve `@mindmakr/reins` to `@mindmakr/reins/stub` (and `@mindmakr/reins/record` to `@mindmakr/reins/record-stub`),
|
|
10
|
+
* with webpack and with Turbopack.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withReins(nextConfig?: NextConfig, options?: ReinsNextOptions): NextConfig;
|
|
13
|
+
export default withReins;
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { keepInBuild } from '@mindmakr/reins/server';
|
|
2
|
+
/** Whether a build should swap reins for its stub. */
|
|
3
|
+
export function shouldStub(dev, options = {}, env = process.env) {
|
|
4
|
+
return !dev && !options.keep && !keepInBuild(env);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a Next.js config. Production builds resolve `@mindmakr/reins` to `@mindmakr/reins/stub` (and `@mindmakr/reins/record` to `@mindmakr/reins/record-stub`),
|
|
8
|
+
* with webpack and with Turbopack.
|
|
9
|
+
*/
|
|
10
|
+
export function withReins(nextConfig = {}, options = {}) {
|
|
11
|
+
const prodBuild = process.env.NODE_ENV === 'production';
|
|
12
|
+
const out = {
|
|
13
|
+
...nextConfig,
|
|
14
|
+
webpack(config, ctx) {
|
|
15
|
+
if (shouldStub(ctx.dev, options)) {
|
|
16
|
+
config.resolve = config.resolve || {};
|
|
17
|
+
config.resolve.alias = { ...(config.resolve.alias || {}), '@mindmakr/reins$': '@mindmakr/reins/stub', '@mindmakr/reins/record$': '@mindmakr/reins/record-stub' };
|
|
18
|
+
}
|
|
19
|
+
return typeof nextConfig.webpack === 'function' ? nextConfig.webpack(config, ctx) : config;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
if (shouldStub(!prodBuild, options)) {
|
|
23
|
+
out.turbopack = { ...(nextConfig.turbopack || {}), resolveAlias: { ...((nextConfig.turbopack || {}).resolveAlias || {}), '@mindmakr/reins/record': '@mindmakr/reins/record-stub', '@mindmakr/reins': '@mindmakr/reins/stub' } };
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export default withReins;
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Action, ActionInfo, ServerReport } from './types.js';
|
|
2
|
+
export declare const VERSION = "0.1.0";
|
|
3
|
+
/** The header the browser core adds to same-origin calls while paired. */
|
|
4
|
+
export declare const TRACE_HEADER = "x-reins-trace";
|
|
5
|
+
export interface NodeConnectOptions {
|
|
6
|
+
/** Bridge WebSocket URL. Default `ws://127.0.0.1:7357`. */
|
|
7
|
+
url?: string;
|
|
8
|
+
/** Pairing token. Default: read from `tokenFile`. */
|
|
9
|
+
token?: string;
|
|
10
|
+
/** Default `<cwd>/.reins/token`, which the bridge writes. Read again before each attempt, because the bridge renews it. */
|
|
11
|
+
tokenFile?: string;
|
|
12
|
+
/** Extra facts for the handshake, such as `{ name: 'api' }`. `platform` is always `node`. */
|
|
13
|
+
meta?: Record<string, string>;
|
|
14
|
+
/** Retry when the bridge is not there or goes away. Default true. */
|
|
15
|
+
reconnect?: boolean;
|
|
16
|
+
/** Query keys and headers shown as they are; everything sensitive is redacted. */
|
|
17
|
+
allow?: {
|
|
18
|
+
queryKeys?: string[];
|
|
19
|
+
headers?: string[];
|
|
20
|
+
};
|
|
21
|
+
/** Requests kept. Default 200. */
|
|
22
|
+
bufferSize?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Record without a bridge, from the moment `connect()` is called. For tests and scripts that read `report()`
|
|
25
|
+
* themselves. Default false: nothing is recorded until the bridge pairs, and recording stops when it goes away.
|
|
26
|
+
*/
|
|
27
|
+
standalone?: boolean;
|
|
28
|
+
/** For tests. Default: the global WebSocket (Node 22+), else the `ws` package. */
|
|
29
|
+
WebSocket?: new (url: string) => any;
|
|
30
|
+
}
|
|
31
|
+
export interface MiddlewareOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Add a `Server-Timing` header (total, `db.*` time and event-loop p99, plus the route) to each response,
|
|
34
|
+
* so a dev HUD can show server time without extra requests. Dev only, numbers and the route only. Default false.
|
|
35
|
+
*/
|
|
36
|
+
serverTiming?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Whether this process may run reins: not in production unless REINS_ENABLE=1 (private staging only). */
|
|
39
|
+
export declare function nodeEnabled(env?: Record<string, string | undefined>): boolean;
|
|
40
|
+
/** Creates an instance. Most apps use the `reinsNode` singleton. */
|
|
41
|
+
export declare function createReinsNode(env?: Record<string, string | undefined>): {
|
|
42
|
+
connect: (o?: NodeConnectOptions) => Promise<boolean>;
|
|
43
|
+
disconnect: () => void;
|
|
44
|
+
clear: () => void;
|
|
45
|
+
isPaired: () => boolean;
|
|
46
|
+
isActive: () => boolean;
|
|
47
|
+
middleware: (o?: MiddlewareOptions) => (req: any, res: any, next: (err?: unknown) => void) => void;
|
|
48
|
+
wrapHandler: <H extends (req: any, res: any, ...rest: any[]) => any>(handler: H, o?: MiddlewareOptions) => H;
|
|
49
|
+
fastify: (app: {
|
|
50
|
+
addHook(name: string, fn: (...a: any[]) => void): unknown;
|
|
51
|
+
}) => {
|
|
52
|
+
addHook(name: string, fn: (...a: any[]) => void): unknown;
|
|
53
|
+
};
|
|
54
|
+
span: <T>(name: string, fn: () => T) => T;
|
|
55
|
+
action: <I = any, O = any>(def: Action<I, O>) => () => void;
|
|
56
|
+
actions: () => ActionInfo[];
|
|
57
|
+
run: (name: string, input?: unknown, confirm?: boolean) => Promise<unknown>;
|
|
58
|
+
report: () => ServerReport;
|
|
59
|
+
};
|
|
60
|
+
export type ReinsNode = ReturnType<typeof createReinsNode>;
|
|
61
|
+
export declare const reinsNode: {
|
|
62
|
+
connect: (o?: NodeConnectOptions) => Promise<boolean>;
|
|
63
|
+
disconnect: () => void;
|
|
64
|
+
clear: () => void;
|
|
65
|
+
isPaired: () => boolean;
|
|
66
|
+
isActive: () => boolean;
|
|
67
|
+
middleware: (o?: MiddlewareOptions) => (req: any, res: any, next: (err?: unknown) => void) => void;
|
|
68
|
+
wrapHandler: <H extends (req: any, res: any, ...rest: any[]) => any>(handler: H, o?: MiddlewareOptions) => H;
|
|
69
|
+
fastify: (app: {
|
|
70
|
+
addHook(name: string, fn: (...a: any[]) => void): unknown;
|
|
71
|
+
}) => {
|
|
72
|
+
addHook(name: string, fn: (...a: any[]) => void): unknown;
|
|
73
|
+
};
|
|
74
|
+
span: <T>(name: string, fn: () => T) => T;
|
|
75
|
+
action: <I = any, O = any>(def: Action<I, O>) => () => void;
|
|
76
|
+
actions: () => ActionInfo[];
|
|
77
|
+
run: (name: string, input?: unknown, confirm?: boolean) => Promise<unknown>;
|
|
78
|
+
report: () => ServerReport;
|
|
79
|
+
};
|
|
80
|
+
export default reinsNode;
|