@usereelay/browser 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/README.md +152 -0
- package/bin/reelay-sourcemaps.mjs +277 -0
- package/dist/index.cjs +14077 -0
- package/dist/index.d.cts +264 -0
- package/dist/index.d.ts +264 -0
- package/dist/index.js +14071 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @usereelay/browser
|
|
2
|
+
|
|
3
|
+
Reelay error tracking for browsers. Captures uncaught errors and unhandled
|
|
4
|
+
promise rejections, records click/console/fetch breadcrumbs and a
|
|
5
|
+
masked-by-default DOM session replay, and stitches frontend sessions to
|
|
6
|
+
backend traces — with **zero runtime dependencies** and a hard rule of zero
|
|
7
|
+
host-app disruption.
|
|
8
|
+
|
|
9
|
+
Standalone project: everything the SDK needs (transport, PII scrubbing,
|
|
10
|
+
stack parsing, trace ids, replay) is built in. Ships as a single ESM bundle
|
|
11
|
+
(~22 KB unminified) plus CJS + types.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# from npm (when published)
|
|
17
|
+
npm install @usereelay/browser
|
|
18
|
+
|
|
19
|
+
# or as a local file dependency
|
|
20
|
+
npm install /path/to/reelay-browser-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import * as Reelay from '@usereelay/browser';
|
|
27
|
+
|
|
28
|
+
Reelay.init({
|
|
29
|
+
endpoint: 'https://api.usereelay.com', // your Reelay ingest base URL
|
|
30
|
+
token: 'rlyt_live_…', // node API key for this frontend
|
|
31
|
+
release: 'web@' + import.meta.env.VITE_GIT_SHA,
|
|
32
|
+
environment: 'production',
|
|
33
|
+
// Origins that receive trace headers so backend errors link to this session:
|
|
34
|
+
tracePropagationTargets: ['https://api.acme.com'],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Call `init()` once, as early as possible (before your app boots). Everything
|
|
39
|
+
else is automatic. The built `dist/index.js` is dependency-free ESM, so it
|
|
40
|
+
also works without a bundler:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script type="module">
|
|
44
|
+
import * as Reelay from '/vendor/reelay-browser.js';
|
|
45
|
+
Reelay.init({ endpoint: '…', token: '…' });
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What each piece does
|
|
50
|
+
|
|
51
|
+
| Export / feature | Purpose |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `init(options)` | Creates the singleton client and installs all instrumentation. Idempotent — repeat calls return the existing client. |
|
|
54
|
+
| Global error capture | `window` `error` + `unhandledrejection` via `addEventListener` — never by overwriting `window.onerror`, so other tooling keeps working. |
|
|
55
|
+
| Breadcrumbs | Clicks (element descriptor only, no text), `popstate` navigations, `console.error`/`console.warn` (wrapped, originals preserved), and every `fetch` (method + URL + status). Ring buffer of the latest 50, attached to every event. |
|
|
56
|
+
| Fetch trace stitching | For URLs matching `tracePropagationTargets`, outgoing requests get W3C `traceparent` + `reelay-session-id` headers. The `@usereelay/node` middleware adopts them, so a backend error and this page's replay share one `trace_id`. Same-origin only by default; **never** cross-origin unless you allow-list it. |
|
|
57
|
+
| Session replay | Initial DOM snapshot + `MutationObserver` deltas, serialized inside `requestIdleCallback`, shipped as sequenced chunks every 5 s. With `maskAllText` (default) every text node is masked **before** serialization and inputs are always masked — content never leaves the page. |
|
|
58
|
+
| `captureException(err)` | Manually report a handled error. Returns the event id (`''` if dropped). |
|
|
59
|
+
| `captureMessage(msg)` | Report a string message as an event. |
|
|
60
|
+
| `getClient()` | The active `BrowserClient` (exposes `sessionId`, `addBreadcrumb`, `flush`, `uninstall`). |
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
Reelay.init({
|
|
66
|
+
endpoint: 'https://api.usereelay.com', // required: ingest base URL
|
|
67
|
+
token: 'rlyt_live_…', // required: node API key
|
|
68
|
+
release: 'web@abc123', // optional: ties events to a deploy
|
|
69
|
+
environment: 'production', // optional
|
|
70
|
+
sampleRate: 1, // 0–1 probability an event is sent
|
|
71
|
+
beforeSend: (event) => event, // mutate or return null to drop
|
|
72
|
+
scrubPatterns: [/order-\d{6}/g], // extra PII regexes (additive)
|
|
73
|
+
maxQueueSize: 30, // offline/retry buffer (drop-oldest)
|
|
74
|
+
tracePropagationTargets: [ // origins/patterns that get trace headers
|
|
75
|
+
'https://api.acme.com',
|
|
76
|
+
/^https:\/\/.*\.internal\.acme\.com/,
|
|
77
|
+
],
|
|
78
|
+
replay: true, // DOM session replay on/off
|
|
79
|
+
maskAllText: true, // mask every text node in replay
|
|
80
|
+
debug: (msg, detail) => {}, // SDK self-diagnostics (silent by default)
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Engineering guarantees
|
|
85
|
+
|
|
86
|
+
- **Zero host disruption** — every listener and public API runs inside a
|
|
87
|
+
defensive boundary; SDK failures go to the `debug` hook, never your app.
|
|
88
|
+
`fetch` wrapping preserves the original's exact arguments/return; failed
|
|
89
|
+
instrumentation degrades to a passthrough call.
|
|
90
|
+
- **Performance budget** — replay serialization runs in `requestIdleCallback`,
|
|
91
|
+
click listeners are passive + capture-phase, breadcrumbs are an O(1) ring
|
|
92
|
+
buffer; nothing blocks the UI thread, so LCP/INP/CLS are unaffected.
|
|
93
|
+
- **No data loss on navigation** — events flush on `visibilitychange: hidden`
|
|
94
|
+
using `keepalive` fetch, which survives page unload.
|
|
95
|
+
- **PII never leaves the page** — sensitive keys redacted wholesale; tokens,
|
|
96
|
+
JWTs, card numbers and emails pattern-scrubbed; replay masks all text and
|
|
97
|
+
all input values by default.
|
|
98
|
+
- **Bounded memory** — breadcrumb ring (50), transport queue (30,
|
|
99
|
+
drop-oldest), replay delta buffer (500), traced-request map (100).
|
|
100
|
+
|
|
101
|
+
## Source maps (frontend symbolication)
|
|
102
|
+
|
|
103
|
+
Production bundles are minified, so raw stack frames point at hashed files like
|
|
104
|
+
`assets/index-a1b2c3.js:1:54023`. To see original files, lines, and function
|
|
105
|
+
names in Reelay, provision your source maps at build/deploy time with the
|
|
106
|
+
bundled `reelay-sourcemaps` CLI.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. Build your app with source maps enabled (prefer hidden-source-map so the
|
|
110
|
+
# maps are emitted but not referenced from shipped JS).
|
|
111
|
+
npm run build
|
|
112
|
+
|
|
113
|
+
# 2. Inject debug ids into the built JS + maps (the robust, release-independent
|
|
114
|
+
# match key), then upload the maps to Reelay.
|
|
115
|
+
npx reelay-sourcemaps inject-and-upload ./dist \
|
|
116
|
+
--url https://api.reelay.app \
|
|
117
|
+
--token "$REELAY_TOKEN" \
|
|
118
|
+
--release "web@$(git rev-parse --short HEAD)" \
|
|
119
|
+
--delete # remove .map files after upload so they never reach your CDN
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`--url`, `--token`, and `--release` also read from `REELAY_URL`, `REELAY_TOKEN`,
|
|
123
|
+
and `REELAY_RELEASE`. Set the **same `release`** in `Reelay.init({ release })`
|
|
124
|
+
so release-based matching works even for bundles without debug ids.
|
|
125
|
+
|
|
126
|
+
How it works:
|
|
127
|
+
|
|
128
|
+
- **Debug ids (preferred).** `inject` stamps each chunk with a content-derived
|
|
129
|
+
id, recorded both in the JS (a tiny runtime snippet) and in the map. The SDK
|
|
130
|
+
reads that registry at capture time and tags each frame with its bundle's
|
|
131
|
+
debug id, so the backend resolves the exact map regardless of release naming
|
|
132
|
+
or CDN hashing. The injector offsets the map's `mappings` for the one line it
|
|
133
|
+
adds, so positions stay exact.
|
|
134
|
+
- **Release + filename (fallback).** When a frame has no debug id, the backend
|
|
135
|
+
matches on the event's `release` and the minified file's basename.
|
|
136
|
+
- Already using a Sentry bundler plugin? The SDK also reads `_sentryDebugIds`,
|
|
137
|
+
so existing debug ids are picked up with no extra tooling.
|
|
138
|
+
|
|
139
|
+
Source maps expose original source — Reelay stores them privately, never serves
|
|
140
|
+
them back, and expires them on the same retention window as your events.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm install
|
|
146
|
+
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
|
|
147
|
+
npm test # vitest
|
|
148
|
+
npm run typecheck
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
See `../reelay-test-project/frontend` for a runnable page wired up with this
|
|
152
|
+
SDK (including buttons that trigger every class of frontend error).
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* reelay-sourcemaps — build/deploy-time source-map provisioning for the Reelay
|
|
4
|
+
* browser SDK. Zero dependencies; runs on Node 18+ (uses global fetch).
|
|
5
|
+
*
|
|
6
|
+
* reelay-sourcemaps inject <dir> Inject debug ids into built JS + maps
|
|
7
|
+
* reelay-sourcemaps upload <dir> [opts] Upload maps to Reelay
|
|
8
|
+
* reelay-sourcemaps inject-and-upload <dir> Both, in order
|
|
9
|
+
*
|
|
10
|
+
* Options (or env: REELAY_URL, REELAY_TOKEN, REELAY_RELEASE):
|
|
11
|
+
* --url <endpoint> Reelay ingestion base URL [REELAY_URL]
|
|
12
|
+
* --token <token> Node API key (rlyt_live_…) [REELAY_TOKEN]
|
|
13
|
+
* --release <release> Release tag matching your SDK init [REELAY_RELEASE]
|
|
14
|
+
* --delete Delete .map files after a successful upload
|
|
15
|
+
*
|
|
16
|
+
* Debug ids are the primary, unambiguous match key: each bundle is stamped with
|
|
17
|
+
* a content-derived id recorded both in the JS (a tiny runtime snippet the SDK
|
|
18
|
+
* reads) and in the map. `upload` also sends release+filename as a fallback, so
|
|
19
|
+
* symbolication works even for bundlers that emit no debug ids.
|
|
20
|
+
*
|
|
21
|
+
* Source maps expose original source — keep them private. Prefer `hidden-source-map`
|
|
22
|
+
* (no sourceMappingURL comment in shipped JS) and pass --delete so maps never
|
|
23
|
+
* reach your CDN.
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
27
|
+
import { join, basename, dirname, resolve } from 'node:path';
|
|
28
|
+
|
|
29
|
+
const JS_EXT = /\.(?:js|mjs|cjs)$/;
|
|
30
|
+
const SOURCE_MAPPING_URL = /\/\/# sourceMappingURL=([^\s'"]+)\s*$/m;
|
|
31
|
+
const DEBUG_ID_COMMENT = /\/\/# debugId=([0-9a-fA-F-]+)/;
|
|
32
|
+
|
|
33
|
+
function log(msg) {
|
|
34
|
+
process.stdout.write(`[reelay-sourcemaps] ${msg}\n`);
|
|
35
|
+
}
|
|
36
|
+
function warn(msg) {
|
|
37
|
+
process.stderr.write(`[reelay-sourcemaps] WARN ${msg}\n`);
|
|
38
|
+
}
|
|
39
|
+
function fail(msg) {
|
|
40
|
+
process.stderr.write(`[reelay-sourcemaps] ERROR ${msg}\n`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Deterministic UUIDv4-shaped id from content, so re-runs are idempotent. */
|
|
45
|
+
function debugIdFromContent(buf) {
|
|
46
|
+
const h = createHash('sha256').update(buf).digest();
|
|
47
|
+
const b = Buffer.from(h.subarray(0, 16));
|
|
48
|
+
b[6] = (b[6] & 0x0f) | 0x40; // version 4
|
|
49
|
+
b[8] = (b[8] & 0x3f) | 0x80; // variant 10
|
|
50
|
+
const hex = b.toString('hex');
|
|
51
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walk(dir, out = []) {
|
|
55
|
+
for (const entry of readdirSync(dir)) {
|
|
56
|
+
const full = join(dir, entry);
|
|
57
|
+
const st = statSync(full);
|
|
58
|
+
if (st.isDirectory()) walk(full, out);
|
|
59
|
+
else out.push(full);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolves the map path for a JS file via its sourceMappingURL or `<js>.map`. */
|
|
65
|
+
function mapPathFor(jsFile, jsText) {
|
|
66
|
+
const m = SOURCE_MAPPING_URL.exec(jsText);
|
|
67
|
+
if (m && m[1] && !m[1].startsWith('data:')) {
|
|
68
|
+
return resolve(dirname(jsFile), m[1]);
|
|
69
|
+
}
|
|
70
|
+
const sibling = `${jsFile}.map`;
|
|
71
|
+
try {
|
|
72
|
+
statSync(sibling);
|
|
73
|
+
return sibling;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Runtime snippet recording this chunk's debug id, keyed by a freshly-captured
|
|
81
|
+
* stack so the SDK can map script URL -> debug id. One physical line; we prepend
|
|
82
|
+
* exactly one `;` to the map's mappings to offset the inserted generated line.
|
|
83
|
+
*/
|
|
84
|
+
function injectionSnippet(debugId) {
|
|
85
|
+
return (
|
|
86
|
+
`;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:` +
|
|
87
|
+
`"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},` +
|
|
88
|
+
`n=(new e.Error).stack;n&&(e._reelayDebugIds=e._reelayDebugIds||{},` +
|
|
89
|
+
`e._reelayDebugIds[n]="${debugId}")}catch(e){}}();`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function injectPair(jsFile, jsText) {
|
|
94
|
+
const existing = DEBUG_ID_COMMENT.exec(jsText);
|
|
95
|
+
const mapFile = mapPathFor(jsFile, jsText);
|
|
96
|
+
if (!mapFile) {
|
|
97
|
+
return { status: 'skipped', reason: 'no source map' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let map;
|
|
101
|
+
try {
|
|
102
|
+
map = JSON.parse(readFileSync(mapFile, 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
return { status: 'skipped', reason: 'unparseable map' };
|
|
105
|
+
}
|
|
106
|
+
if (map.version !== 3 || typeof map.mappings !== 'string') {
|
|
107
|
+
// Indexed maps (sections) would need per-section offsetting; skip rather
|
|
108
|
+
// than risk corrupting the mapping.
|
|
109
|
+
return { status: 'skipped', reason: 'unsupported map (not flat v3)' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Idempotent: if already injected, just make sure the map carries the id.
|
|
113
|
+
if (existing) {
|
|
114
|
+
const debugId = existing[1];
|
|
115
|
+
if (map.debugId !== debugId || map.debug_id !== debugId) {
|
|
116
|
+
map.debugId = debugId;
|
|
117
|
+
map.debug_id = debugId;
|
|
118
|
+
writeFileSync(mapFile, JSON.stringify(map));
|
|
119
|
+
}
|
|
120
|
+
return { status: 'already', debugId, mapFile };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const debugId = debugIdFromContent(jsText);
|
|
124
|
+
const snippet = injectionSnippet(debugId);
|
|
125
|
+
|
|
126
|
+
// Prepend snippet as one full line; original code keeps its columns and moves
|
|
127
|
+
// down one generated line, which we offset with a single leading ';'.
|
|
128
|
+
let next = `${snippet}\n${jsText}`;
|
|
129
|
+
next = next.replace(/\s*$/, '');
|
|
130
|
+
next += `\n//# debugId=${debugId}\n`;
|
|
131
|
+
writeFileSync(jsFile, next);
|
|
132
|
+
|
|
133
|
+
map.mappings = `;${map.mappings}`;
|
|
134
|
+
map.debugId = debugId;
|
|
135
|
+
map.debug_id = debugId;
|
|
136
|
+
writeFileSync(mapFile, JSON.stringify(map));
|
|
137
|
+
|
|
138
|
+
return { status: 'injected', debugId, mapFile };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cmdInject(dir) {
|
|
142
|
+
let injected = 0;
|
|
143
|
+
let already = 0;
|
|
144
|
+
for (const file of walk(dir)) {
|
|
145
|
+
if (!JS_EXT.test(file)) continue;
|
|
146
|
+
let jsText;
|
|
147
|
+
try {
|
|
148
|
+
jsText = readFileSync(file, 'utf8');
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const res = injectPair(file, jsText);
|
|
153
|
+
if (res.status === 'injected') {
|
|
154
|
+
injected++;
|
|
155
|
+
log(`injected ${res.debugId} -> ${basename(file)}`);
|
|
156
|
+
} else if (res.status === 'already') {
|
|
157
|
+
already++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
log(`inject complete: ${injected} injected, ${already} already done`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function uploadMap(opts, mapFile) {
|
|
164
|
+
let map;
|
|
165
|
+
try {
|
|
166
|
+
map = JSON.parse(readFileSync(mapFile, 'utf8'));
|
|
167
|
+
} catch {
|
|
168
|
+
warn(`skipping unreadable map ${mapFile}`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const debugId = map.debugId || map.debug_id || '';
|
|
172
|
+
// Minified file the map belongs to: prefer the map's own `file`, else strip
|
|
173
|
+
// the .map suffix. Sent as the release+filename fallback key.
|
|
174
|
+
const file = basename(map.file || mapFile.replace(/\.map$/, ''));
|
|
175
|
+
|
|
176
|
+
if (!debugId && !opts.release) {
|
|
177
|
+
warn(`skipping ${basename(mapFile)}: no debug id and no --release to key on`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const body = JSON.stringify({
|
|
182
|
+
release: opts.release || undefined,
|
|
183
|
+
debug_id: debugId || undefined,
|
|
184
|
+
file,
|
|
185
|
+
sourcemap: readFileSync(mapFile, 'utf8'),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const res = await fetch(`${opts.url.replace(/\/$/, '')}/api/ingest/sourcemaps`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json', 'X-Reelay-Token': opts.token },
|
|
191
|
+
body,
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const text = await res.text().catch(() => '');
|
|
195
|
+
warn(`upload failed for ${basename(mapFile)}: ${res.status} ${text.slice(0, 200)}`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
log(`uploaded ${basename(mapFile)}${debugId ? ` (debugId ${debugId})` : ''}`);
|
|
199
|
+
if (opts.delete) rmSync(mapFile);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function cmdUpload(dir, opts) {
|
|
204
|
+
if (!opts.url) fail('missing --url (or REELAY_URL)');
|
|
205
|
+
if (!opts.token) fail('missing --token (or REELAY_TOKEN)');
|
|
206
|
+
|
|
207
|
+
const maps = walk(dir).filter((f) => f.endsWith('.map'));
|
|
208
|
+
if (maps.length === 0) {
|
|
209
|
+
warn(`no .map files found under ${dir}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
let ok = 0;
|
|
213
|
+
for (const mapFile of maps) {
|
|
214
|
+
if (await uploadMap(opts, mapFile)) ok++;
|
|
215
|
+
}
|
|
216
|
+
log(`upload complete: ${ok}/${maps.length} maps stored`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseArgs(argv) {
|
|
220
|
+
const opts = {
|
|
221
|
+
url: process.env.REELAY_URL || process.env.REELAY_ENDPOINT || '',
|
|
222
|
+
token: process.env.REELAY_TOKEN || '',
|
|
223
|
+
release: process.env.REELAY_RELEASE || '',
|
|
224
|
+
delete: false,
|
|
225
|
+
_: [],
|
|
226
|
+
};
|
|
227
|
+
for (let i = 0; i < argv.length; i++) {
|
|
228
|
+
const a = argv[i];
|
|
229
|
+
switch (a) {
|
|
230
|
+
case '--url': opts.url = argv[++i]; break;
|
|
231
|
+
case '--token': opts.token = argv[++i]; break;
|
|
232
|
+
case '--release': opts.release = argv[++i]; break;
|
|
233
|
+
case '--delete': opts.delete = true; break;
|
|
234
|
+
default: opts._.push(a);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return opts;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function main() {
|
|
241
|
+
const [, , command, ...rest] = process.argv;
|
|
242
|
+
const opts = parseArgs(rest);
|
|
243
|
+
const dir = opts._[0];
|
|
244
|
+
|
|
245
|
+
if (!command || command === '--help' || command === '-h') {
|
|
246
|
+
process.stdout.write(
|
|
247
|
+
'Usage:\n' +
|
|
248
|
+
' reelay-sourcemaps inject <dir>\n' +
|
|
249
|
+
' reelay-sourcemaps upload <dir> --url <u> --token <t> [--release <r>] [--delete]\n' +
|
|
250
|
+
' reelay-sourcemaps inject-and-upload <dir> --url <u> --token <t> [--release <r>] [--delete]\n',
|
|
251
|
+
);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!dir) fail('missing <dir> argument');
|
|
255
|
+
try {
|
|
256
|
+
statSync(dir);
|
|
257
|
+
} catch {
|
|
258
|
+
fail(`directory not found: ${dir}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
switch (command) {
|
|
262
|
+
case 'inject':
|
|
263
|
+
cmdInject(dir);
|
|
264
|
+
break;
|
|
265
|
+
case 'upload':
|
|
266
|
+
await cmdUpload(dir, opts);
|
|
267
|
+
break;
|
|
268
|
+
case 'inject-and-upload':
|
|
269
|
+
cmdInject(dir);
|
|
270
|
+
await cmdUpload(dir, opts);
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
fail(`unknown command: ${command}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
main().catch((err) => fail(err?.stack || String(err)));
|