@merittdev/horus-lens 0.3.0 → 0.5.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/dist/browser.cjs +440 -185
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +2 -2
- package/dist/{chunk-LIYEDZV7.js → chunk-2G5FEWPU.js} +76 -16
- package/dist/{chunk-LIYEDZV7.js.map → chunk-2G5FEWPU.js.map} +1 -1
- package/dist/{chunk-PNY6BL4C.js → chunk-M3QMIY4Y.js} +367 -172
- package/dist/chunk-M3QMIY4Y.js.map +1 -0
- package/dist/cli.cjs +353 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +75 -15
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-Dyzc1E9T.d.ts → index.d-BYuZWEkh.d.ts} +7 -0
- package/dist/{index.d-BYG_Mjs-.d.cts → index.d-DeJ1mfh0.d.cts} +7 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/dist/lens.min.js +204 -130
- package/dist/react.cjs +440 -185
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/package.json +10 -4
- package/dist/chunk-PNY6BL4C.js.map +0 -1
- package/dist/lens.min.js.map +0 -1
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_node_fs2 = require("fs");
|
|
6
|
+
|
|
7
|
+
// src/cli-lib.ts
|
|
8
|
+
var import_node_fs = require("fs");
|
|
9
|
+
var import_node_path = require("path");
|
|
10
|
+
var DEFAULT_ENDPOINT = "https://api.horus.sh";
|
|
11
|
+
var UPLOAD_KEY_ENV = "LENS_UPLOAD_KEY";
|
|
12
|
+
var MAX_FILES = 200;
|
|
13
|
+
var USAGE = `Usage: horus-lens upload-sourcemaps --release <release> [options] <dir...>
|
|
14
|
+
|
|
15
|
+
Upload JavaScript sourcemaps (*.map) for a release to Horus Lens.
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
<dir...> One or more directories to search recursively for .map files
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--release <release> Release identifier, e.g. a git SHA [required]
|
|
22
|
+
--key <las_...> Per-site upload key (las_-prefixed).
|
|
23
|
+
Falls back to the ${UPLOAD_KEY_ENV} environment variable.
|
|
24
|
+
--endpoint <url> API endpoint (default: ${DEFAULT_ENDPOINT})
|
|
25
|
+
--dry-run List what would be uploaded, then exit without uploading
|
|
26
|
+
-h, --help Show this help
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
npx @merittdev/horus-lens upload-sourcemaps --release "$GIT_SHA" dist/`;
|
|
30
|
+
function parseArgs(argv, env = process.env) {
|
|
31
|
+
if (argv.length === 0) return { kind: "help" };
|
|
32
|
+
const [command, ...rest] = argv;
|
|
33
|
+
if (command === "-h" || command === "--help" || command === "help") {
|
|
34
|
+
return { kind: "help" };
|
|
35
|
+
}
|
|
36
|
+
if (command !== "upload-sourcemaps") {
|
|
37
|
+
return { kind: "error", message: `Unknown command: ${command}` };
|
|
38
|
+
}
|
|
39
|
+
let release;
|
|
40
|
+
let key;
|
|
41
|
+
let endpoint = DEFAULT_ENDPOINT;
|
|
42
|
+
let dryRun = false;
|
|
43
|
+
const dirs = [];
|
|
44
|
+
for (let i = 0; i < rest.length; i++) {
|
|
45
|
+
const token = rest[i];
|
|
46
|
+
if (token === "-h" || token === "--help") return { kind: "help" };
|
|
47
|
+
if (token.startsWith("--")) {
|
|
48
|
+
let flag = token;
|
|
49
|
+
let value;
|
|
50
|
+
const eq = token.indexOf("=");
|
|
51
|
+
if (eq !== -1) {
|
|
52
|
+
flag = token.slice(0, eq);
|
|
53
|
+
value = token.slice(eq + 1);
|
|
54
|
+
}
|
|
55
|
+
if (flag === "--dry-run") {
|
|
56
|
+
dryRun = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (flag === "--release" || flag === "--key" || flag === "--endpoint") {
|
|
60
|
+
if (value === void 0) {
|
|
61
|
+
const next = rest[i + 1];
|
|
62
|
+
if (next === void 0 || next.startsWith("-")) {
|
|
63
|
+
return { kind: "error", message: `Option ${flag} requires a value.` };
|
|
64
|
+
}
|
|
65
|
+
value = next;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
if (flag === "--release") release = value;
|
|
69
|
+
else if (flag === "--key") key = value;
|
|
70
|
+
else endpoint = value;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
return { kind: "error", message: `Unknown option: ${flag}` };
|
|
74
|
+
}
|
|
75
|
+
dirs.push(token);
|
|
76
|
+
}
|
|
77
|
+
const resolvedKey = (key ?? env[UPLOAD_KEY_ENV] ?? "").trim();
|
|
78
|
+
const missing = [];
|
|
79
|
+
if (!release) missing.push("--release <release>");
|
|
80
|
+
if (dirs.length === 0) missing.push("<dir...>");
|
|
81
|
+
if (!dryRun && !resolvedKey) {
|
|
82
|
+
missing.push(`--key <las_...> (or set ${UPLOAD_KEY_ENV})`);
|
|
83
|
+
}
|
|
84
|
+
if (missing.length > 0) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "error",
|
|
87
|
+
message: `Missing required argument(s): ${missing.join(", ")}`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "run",
|
|
92
|
+
args: { release, key: resolvedKey, endpoint, dryRun, dirs }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function discoverSourcemaps(dirs, opts = {}) {
|
|
96
|
+
const maxFiles = opts.maxFiles ?? MAX_FILES;
|
|
97
|
+
const warnings = [];
|
|
98
|
+
const seen = /* @__PURE__ */ new Map();
|
|
99
|
+
const files = [];
|
|
100
|
+
let duplicatesDropped = 0;
|
|
101
|
+
const walk = (dir) => {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = (0, import_node_fs.readdirSync)(dir, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
warnings.push(`could not read directory: ${dir}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.name === "node_modules") continue;
|
|
112
|
+
const full = (0, import_node_path.join)(dir, entry.name);
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
walk(full);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!entry.isFile() || !entry.name.endsWith(".map")) continue;
|
|
118
|
+
const name = entry.name;
|
|
119
|
+
const existing = seen.get(name);
|
|
120
|
+
if (existing !== void 0) {
|
|
121
|
+
duplicatesDropped++;
|
|
122
|
+
warnings.push(
|
|
123
|
+
`DUPLICATE sourcemap basename "${name}" \u2014 the server identifies maps by basename, so only ${existing} will be uploaded; ${full} is SKIPPED and its bundle will have NO sourcemap. Rename the bundle/map to disambiguate.`
|
|
124
|
+
);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let byteSize;
|
|
128
|
+
try {
|
|
129
|
+
byteSize = (0, import_node_fs.statSync)(full).size;
|
|
130
|
+
} catch {
|
|
131
|
+
warnings.push(`could not stat file: ${full}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
seen.set(name, full);
|
|
135
|
+
files.push({ name, byteSize, absolutePath: full });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
for (const dir of dirs) walk((0, import_node_path.resolve)(dir));
|
|
139
|
+
if (duplicatesDropped > 0) {
|
|
140
|
+
warnings.push(
|
|
141
|
+
`${duplicatesDropped} sourcemap(s) SKIPPED due to duplicate basenames \u2014 those bundles will have no sourcemap on the server. See the warnings above.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
let truncated = false;
|
|
145
|
+
let result = files;
|
|
146
|
+
if (files.length > maxFiles) {
|
|
147
|
+
truncated = true;
|
|
148
|
+
warnings.push(
|
|
149
|
+
`found ${files.length} sourcemaps; capping at ${maxFiles} \u2014 the rest will be skipped.`
|
|
150
|
+
);
|
|
151
|
+
result = files.slice(0, maxFiles);
|
|
152
|
+
}
|
|
153
|
+
return { files: result, warnings, truncated, duplicatesDropped };
|
|
154
|
+
}
|
|
155
|
+
function formatBytes(bytes) {
|
|
156
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
|
|
157
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
158
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
159
|
+
let value = bytes / 1024;
|
|
160
|
+
let unitIndex = 0;
|
|
161
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
162
|
+
value /= 1024;
|
|
163
|
+
unitIndex++;
|
|
164
|
+
}
|
|
165
|
+
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/cli.ts
|
|
169
|
+
var PUT_CONCURRENCY = 4;
|
|
170
|
+
async function postJson(url, key, body) {
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(url, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
authorization: `Bearer ${key}`,
|
|
176
|
+
"content-type": "application/json"
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify(body)
|
|
179
|
+
});
|
|
180
|
+
return { ok: res.ok, status: res.status, body: await res.text() };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return { ok: false, status: 0, body: `network error: ${err.message}` };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function putFile(file, target) {
|
|
186
|
+
const bytes = (0, import_node_fs2.readFileSync)(file.absolutePath);
|
|
187
|
+
const method = target.method ?? "PUT";
|
|
188
|
+
const headers = target.headers ?? {};
|
|
189
|
+
let lastError = "";
|
|
190
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(target.url, { method, headers, body: bytes });
|
|
193
|
+
if (res.ok) return;
|
|
194
|
+
lastError = `HTTP ${res.status}`;
|
|
195
|
+
if (res.status < 500) break;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
lastError = err.message;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new Error(lastError || "upload failed");
|
|
201
|
+
}
|
|
202
|
+
async function runPool(items, concurrency, worker) {
|
|
203
|
+
const failures = [];
|
|
204
|
+
let index = 0;
|
|
205
|
+
const next = async () => {
|
|
206
|
+
while (index < items.length) {
|
|
207
|
+
const item = items[index++];
|
|
208
|
+
try {
|
|
209
|
+
await worker(item);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
failures.push({ item, error: err.message });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const runners = [];
|
|
216
|
+
for (let i = 0; i < Math.min(concurrency, items.length); i++) runners.push(next());
|
|
217
|
+
await Promise.all(runners);
|
|
218
|
+
return failures;
|
|
219
|
+
}
|
|
220
|
+
async function main(argv) {
|
|
221
|
+
const parsed = parseArgs(argv);
|
|
222
|
+
if (parsed.kind === "help") {
|
|
223
|
+
process.stdout.write(`${USAGE}
|
|
224
|
+
`);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
if (parsed.kind === "error") {
|
|
228
|
+
process.stderr.write(`error: ${parsed.message}
|
|
229
|
+
|
|
230
|
+
${USAGE}
|
|
231
|
+
`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const { release, key, endpoint, dryRun, dirs } = parsed.args;
|
|
235
|
+
const { files, warnings, duplicatesDropped } = discoverSourcemaps(dirs);
|
|
236
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}
|
|
237
|
+
`);
|
|
238
|
+
if (files.length === 0) {
|
|
239
|
+
process.stderr.write(
|
|
240
|
+
`warning: no .map files found in: ${dirs.join(", ")} \u2014 nothing to upload.
|
|
241
|
+
`
|
|
242
|
+
);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
const totalBytes = files.reduce((sum, f) => sum + f.byteSize, 0);
|
|
246
|
+
if (dryRun) {
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
`Dry run \u2014 would upload ${files.length} sourcemap(s) for release ${release} (${formatBytes(totalBytes)}):
|
|
249
|
+
`
|
|
250
|
+
);
|
|
251
|
+
for (const f of files) {
|
|
252
|
+
process.stdout.write(` ${f.name} (${formatBytes(f.byteSize)})
|
|
253
|
+
`);
|
|
254
|
+
}
|
|
255
|
+
process.stdout.write("Nothing uploaded (--dry-run).\n");
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
const base = endpoint.replace(/\/+$/, "");
|
|
259
|
+
process.stdout.write(
|
|
260
|
+
`Uploading ${files.length} sourcemap(s) for release ${release} to ${base} ...
|
|
261
|
+
`
|
|
262
|
+
);
|
|
263
|
+
const initiate = await postJson(`${base}/v1/lens/sourcemaps/initiate`, key, {
|
|
264
|
+
release,
|
|
265
|
+
files: files.map((f) => ({ name: f.name, byteSize: f.byteSize }))
|
|
266
|
+
});
|
|
267
|
+
if (!initiate.ok) {
|
|
268
|
+
process.stderr.write(
|
|
269
|
+
`error: initiate failed (HTTP ${initiate.status})
|
|
270
|
+
${initiate.body}
|
|
271
|
+
`
|
|
272
|
+
);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
let initiateData;
|
|
276
|
+
try {
|
|
277
|
+
initiateData = JSON.parse(initiate.body);
|
|
278
|
+
} catch {
|
|
279
|
+
process.stderr.write(
|
|
280
|
+
`error: could not parse initiate response as JSON
|
|
281
|
+
${initiate.body}
|
|
282
|
+
`
|
|
283
|
+
);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
const byName = new Map(files.map((f) => [f.name, f]));
|
|
287
|
+
const jobs = [];
|
|
288
|
+
for (const target of initiateData.uploads ?? []) {
|
|
289
|
+
const file = byName.get(target.name);
|
|
290
|
+
if (!file) {
|
|
291
|
+
process.stderr.write(
|
|
292
|
+
`warning: server returned an upload target for unknown file "${target.name}" \u2014 skipping
|
|
293
|
+
`
|
|
294
|
+
);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
jobs.push({ file, target });
|
|
298
|
+
}
|
|
299
|
+
const targeted = new Set(jobs.map((j) => j.file.name));
|
|
300
|
+
const undelivered = files.filter((f) => !targeted.has(f.name));
|
|
301
|
+
if (undelivered.length > 0) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
`error: server returned upload targets for ${targeted.size} of ${files.length} file(s); no target for: ${undelivered.map((f) => f.name).join(", ")}
|
|
304
|
+
`
|
|
305
|
+
);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const failures = await runPool(jobs, PUT_CONCURRENCY, async ({ file, target }) => {
|
|
309
|
+
await putFile(file, target);
|
|
310
|
+
process.stdout.write(` \u2713 ${file.name}
|
|
311
|
+
`);
|
|
312
|
+
});
|
|
313
|
+
if (failures.length > 0) {
|
|
314
|
+
for (const { item, error } of failures) {
|
|
315
|
+
process.stderr.write(`error: failed to upload ${item.file.name}: ${error}
|
|
316
|
+
`);
|
|
317
|
+
}
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
const uploaded = jobs.map((j) => j.file);
|
|
321
|
+
const finalize = await postJson(`${base}/v1/lens/sourcemaps/finalize`, key, {
|
|
322
|
+
release,
|
|
323
|
+
files: uploaded.map((f) => ({ name: f.name, byteSize: f.byteSize }))
|
|
324
|
+
});
|
|
325
|
+
if (!finalize.ok) {
|
|
326
|
+
process.stderr.write(
|
|
327
|
+
`error: finalize failed (HTTP ${finalize.status})
|
|
328
|
+
${finalize.body}
|
|
329
|
+
`
|
|
330
|
+
);
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
const uploadedBytes = uploaded.reduce((sum, f) => sum + f.byteSize, 0);
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
`\u2713 uploaded ${uploaded.length} sourcemaps for release ${release} (${formatBytes(uploadedBytes)})
|
|
336
|
+
`
|
|
337
|
+
);
|
|
338
|
+
if (duplicatesDropped > 0) {
|
|
339
|
+
process.stdout.write(
|
|
340
|
+
` note: ${duplicatesDropped} sourcemap(s) were skipped due to duplicate basenames (see warnings above).
|
|
341
|
+
`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
main(process.argv.slice(2)).then((code) => {
|
|
347
|
+
process.exitCode = code;
|
|
348
|
+
}).catch((err) => {
|
|
349
|
+
process.stderr.write(`error: ${err.message}
|
|
350
|
+
`);
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
});
|
|
353
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/cli-lib.ts"],"sourcesContent":["/**\n * `horus-lens upload-sourcemaps` — CI-friendly sourcemap uploader.\n *\n * npx @merittdev/horus-lens upload-sourcemaps --release <sha> dist/\n *\n * Node >= 18: uses native `fetch` and `node:fs` recursion — no runtime deps.\n * Pure logic (arg parsing, discovery, byte formatting) lives in `./cli-lib`.\n */\nimport { readFileSync } from 'node:fs';\n\nimport {\n discoverSourcemaps,\n formatBytes,\n parseArgs,\n USAGE,\n type DiscoveredFile,\n} from './cli-lib.js';\n\nconst PUT_CONCURRENCY = 4;\n\ninterface UploadTarget {\n name: string;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\ninterface InitiateResponse {\n uploads?: UploadTarget[];\n}\n\ninterface HttpResult {\n ok: boolean;\n status: number;\n body: string;\n}\n\nasync function postJson(url: string, key: string, body: unknown): Promise<HttpResult> {\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${key}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n return { ok: res.ok, status: res.status, body: await res.text() };\n } catch (err) {\n return { ok: false, status: 0, body: `network error: ${(err as Error).message}` };\n }\n}\n\n/**\n * PUT one file's raw bytes to its presigned URL, retrying once on *transient*\n * failure only.\n *\n * Headers come from the server: presigned URLs are signed with the exact\n * headers the storage backend expects, so we send `target.headers` verbatim and\n * do NOT invent a `content-type` (a wrong/extra Content-Type breaks the\n * signature on S3-style URLs). If the server said nothing, send nothing.\n */\nasync function putFile(file: DiscoveredFile, target: UploadTarget): Promise<void> {\n const bytes = readFileSync(file.absolutePath);\n const method = target.method ?? 'PUT';\n const headers = target.headers ?? {};\n let lastError = '';\n\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const res = await fetch(target.url, { method, headers, body: bytes });\n if (res.ok) return;\n lastError = `HTTP ${res.status}`;\n // Only network failures and 5xx are worth a retry. A 4xx is a permanent\n // client error (expired URL, bad signature, wrong headers) — retrying\n // just wastes time and hides the real cause, so fail fast.\n if (res.status < 500) break;\n } catch (err) {\n lastError = (err as Error).message;\n }\n }\n throw new Error(lastError || 'upload failed');\n}\n\n/** Run `worker` over `items` with bounded concurrency; collect failures. */\nasync function runPool<T>(\n items: T[],\n concurrency: number,\n worker: (item: T) => Promise<void>,\n): Promise<{ item: T; error: string }[]> {\n const failures: { item: T; error: string }[] = [];\n let index = 0;\n\n const next = async (): Promise<void> => {\n while (index < items.length) {\n const item = items[index++]!;\n try {\n await worker(item);\n } catch (err) {\n failures.push({ item, error: (err as Error).message });\n }\n }\n };\n\n const runners: Promise<void>[] = [];\n for (let i = 0; i < Math.min(concurrency, items.length); i++) runners.push(next());\n await Promise.all(runners);\n return failures;\n}\n\nasync function main(argv: string[]): Promise<number> {\n const parsed = parseArgs(argv);\n if (parsed.kind === 'help') {\n process.stdout.write(`${USAGE}\\n`);\n return 0;\n }\n if (parsed.kind === 'error') {\n process.stderr.write(`error: ${parsed.message}\\n\\n${USAGE}\\n`);\n return 1;\n }\n\n const { release, key, endpoint, dryRun, dirs } = parsed.args;\n\n const { files, warnings, duplicatesDropped } = discoverSourcemaps(dirs);\n for (const w of warnings) process.stderr.write(`warning: ${w}\\n`);\n\n // Zero files is not a CI failure — a build may legitimately emit no maps.\n if (files.length === 0) {\n process.stderr.write(\n `warning: no .map files found in: ${dirs.join(', ')} — nothing to upload.\\n`,\n );\n return 0;\n }\n\n const totalBytes = files.reduce((sum, f) => sum + f.byteSize, 0);\n\n if (dryRun) {\n process.stdout.write(\n `Dry run — would upload ${files.length} sourcemap(s) for release ${release} (${formatBytes(totalBytes)}):\\n`,\n );\n for (const f of files) {\n process.stdout.write(` ${f.name} (${formatBytes(f.byteSize)})\\n`);\n }\n process.stdout.write('Nothing uploaded (--dry-run).\\n');\n return 0;\n }\n\n const base = endpoint.replace(/\\/+$/, '');\n process.stdout.write(\n `Uploading ${files.length} sourcemap(s) for release ${release} to ${base} ...\\n`,\n );\n\n // 1. initiate — exchange file list for presigned upload targets.\n const initiate = await postJson(`${base}/v1/lens/sourcemaps/initiate`, key, {\n release,\n files: files.map((f) => ({ name: f.name, byteSize: f.byteSize })),\n });\n if (!initiate.ok) {\n process.stderr.write(\n `error: initiate failed (HTTP ${initiate.status})\\n${initiate.body}\\n`,\n );\n return 1;\n }\n\n let initiateData: InitiateResponse;\n try {\n initiateData = JSON.parse(initiate.body) as InitiateResponse;\n } catch {\n process.stderr.write(\n `error: could not parse initiate response as JSON\\n${initiate.body}\\n`,\n );\n return 1;\n }\n\n const byName = new Map(files.map((f) => [f.name, f]));\n const jobs: { file: DiscoveredFile; target: UploadTarget }[] = [];\n for (const target of initiateData.uploads ?? []) {\n const file = byName.get(target.name);\n if (!file) {\n process.stderr.write(\n `warning: server returned an upload target for unknown file \"${target.name}\" — skipping\\n`,\n );\n continue;\n }\n jobs.push({ file, target });\n }\n\n // Every discovered file must have its own upload target. If the server\n // returned fewer targets than files (or omitted some), finalizing anyway\n // would commit a release with missing sourcemaps and still exit 0 — the\n // failure mode this guard exists to prevent. Fail loudly with the gap listed.\n const targeted = new Set(jobs.map((j) => j.file.name));\n const undelivered = files.filter((f) => !targeted.has(f.name));\n if (undelivered.length > 0) {\n process.stderr.write(\n `error: server returned upload targets for ${targeted.size} of ${files.length} file(s); ` +\n `no target for: ${undelivered.map((f) => f.name).join(', ')}\\n`,\n );\n return 1;\n }\n\n // 2. PUT each file (concurrency-limited, one retry each).\n const failures = await runPool(jobs, PUT_CONCURRENCY, async ({ file, target }) => {\n await putFile(file, target);\n process.stdout.write(` ✓ ${file.name}\\n`);\n });\n if (failures.length > 0) {\n for (const { item, error } of failures) {\n process.stderr.write(`error: failed to upload ${item.file.name}: ${error}\\n`);\n }\n return 1;\n }\n\n // 3. finalize — commit the release with the files we actually uploaded.\n const uploaded = jobs.map((j) => j.file);\n const finalize = await postJson(`${base}/v1/lens/sourcemaps/finalize`, key, {\n release,\n files: uploaded.map((f) => ({ name: f.name, byteSize: f.byteSize })),\n });\n if (!finalize.ok) {\n process.stderr.write(\n `error: finalize failed (HTTP ${finalize.status})\\n${finalize.body}\\n`,\n );\n return 1;\n }\n\n const uploadedBytes = uploaded.reduce((sum, f) => sum + f.byteSize, 0);\n process.stdout.write(\n `✓ uploaded ${uploaded.length} sourcemaps for release ${release} (${formatBytes(uploadedBytes)})\\n`,\n );\n if (duplicatesDropped > 0) {\n process.stdout.write(\n ` note: ${duplicatesDropped} sourcemap(s) were skipped due to duplicate basenames (see warnings above).\\n`,\n );\n }\n return 0;\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`error: ${(err as Error).message}\\n`);\n process.exitCode = 1;\n });\n","/**\n * Pure, side-effect-light logic for the `upload-sourcemaps` CLI: argument\n * parsing, filesystem discovery of `*.map` files, and human byte formatting.\n *\n * Kept separate from `cli.ts` (which owns process/network I/O) so this logic\n * is unit-testable without spawning a subprocess or hitting the network.\n */\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\n\n/**\n * Default Horus Lens API endpoint. Mirrors `DEFAULT_ENDPOINT` in\n * `@horus-lens/core` (packages/core/src/constants.ts). Duplicated as a literal\n * rather than imported because core's only export entrypoint pulls in the full\n * browser SDK (rrweb / DOM globals), which has no place in a Node CLI bundle.\n * Keep this value in sync with core.\n */\nexport const DEFAULT_ENDPOINT = 'https://api.horus.sh';\n\n/** Environment variable consulted for the upload key when `--key` is omitted. */\nexport const UPLOAD_KEY_ENV = 'LENS_UPLOAD_KEY';\n\n/** Hard cap on the number of sourcemaps uploaded per invocation. */\nexport const MAX_FILES = 200;\n\nexport const USAGE = `Usage: horus-lens upload-sourcemaps --release <release> [options] <dir...>\n\nUpload JavaScript sourcemaps (*.map) for a release to Horus Lens.\n\nArguments:\n <dir...> One or more directories to search recursively for .map files\n\nOptions:\n --release <release> Release identifier, e.g. a git SHA [required]\n --key <las_...> Per-site upload key (las_-prefixed).\n Falls back to the ${UPLOAD_KEY_ENV} environment variable.\n --endpoint <url> API endpoint (default: ${DEFAULT_ENDPOINT})\n --dry-run List what would be uploaded, then exit without uploading\n -h, --help Show this help\n\nExample:\n npx @merittdev/horus-lens upload-sourcemaps --release \"$GIT_SHA\" dist/`;\n\nexport interface ParsedArgs {\n release: string;\n /** Resolved upload key (from --key or the env fallback). May be empty for --dry-run. */\n key: string;\n endpoint: string;\n dryRun: boolean;\n dirs: string[];\n}\n\nexport type ParseResult =\n | { kind: 'run'; args: ParsedArgs }\n | { kind: 'help' }\n | { kind: 'error'; message: string };\n\n/**\n * Parse the CLI argv (everything after the binary name) into a structured\n * result. Never throws — validation failures become `{ kind: 'error' }`.\n */\nexport function parseArgs(\n argv: string[],\n env: Record<string, string | undefined> = process.env,\n): ParseResult {\n if (argv.length === 0) return { kind: 'help' };\n\n const [command, ...rest] = argv;\n if (command === '-h' || command === '--help' || command === 'help') {\n return { kind: 'help' };\n }\n if (command !== 'upload-sourcemaps') {\n return { kind: 'error', message: `Unknown command: ${command}` };\n }\n\n let release: string | undefined;\n let key: string | undefined;\n let endpoint = DEFAULT_ENDPOINT;\n let dryRun = false;\n const dirs: string[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const token = rest[i]!;\n\n if (token === '-h' || token === '--help') return { kind: 'help' };\n\n if (token.startsWith('--')) {\n let flag = token;\n let value: string | undefined;\n const eq = token.indexOf('=');\n if (eq !== -1) {\n flag = token.slice(0, eq);\n value = token.slice(eq + 1);\n }\n\n if (flag === '--dry-run') {\n dryRun = true;\n continue;\n }\n\n if (flag === '--release' || flag === '--key' || flag === '--endpoint') {\n if (value === undefined) {\n const next = rest[i + 1];\n if (next === undefined || next.startsWith('-')) {\n return { kind: 'error', message: `Option ${flag} requires a value.` };\n }\n value = next;\n i++;\n }\n if (flag === '--release') release = value;\n else if (flag === '--key') key = value;\n else endpoint = value;\n continue;\n }\n\n return { kind: 'error', message: `Unknown option: ${flag}` };\n }\n\n dirs.push(token);\n }\n\n const resolvedKey = (key ?? env[UPLOAD_KEY_ENV] ?? '').trim();\n\n const missing: string[] = [];\n if (!release) missing.push('--release <release>');\n if (dirs.length === 0) missing.push('<dir...>');\n // The key is only needed to actually talk to the server; --dry-run is offline.\n if (!dryRun && !resolvedKey) {\n missing.push(`--key <las_...> (or set ${UPLOAD_KEY_ENV})`);\n }\n\n if (missing.length > 0) {\n return {\n kind: 'error',\n message: `Missing required argument(s): ${missing.join(', ')}`,\n };\n }\n\n return {\n kind: 'run',\n args: { release: release!, key: resolvedKey, endpoint, dryRun, dirs },\n };\n}\n\nexport interface DiscoveredFile {\n /** Basename, e.g. `main.js.map`. Sent to the server as the file identity. */\n name: string;\n byteSize: number;\n absolutePath: string;\n}\n\nexport interface DiscoveryResult {\n files: DiscoveredFile[];\n warnings: string[];\n truncated: boolean;\n /**\n * Number of files skipped because another file with the same basename was\n * already discovered. The server identifies maps by basename (see\n * `DiscoveredFile.name`), so these collisions cannot be uploaded and their\n * bundles will have no sourcemap. Surfaced so callers can flag it in a summary.\n */\n duplicatesDropped: number;\n}\n\n/**\n * Recursively discover `*.map` files under the given directories.\n *\n * - `node_modules` directories are skipped entirely.\n * - Duplicate basenames (across or within dirs) keep the first seen and warn.\n * - Results are capped at `maxFiles` (warns + truncates rather than failing).\n * - Unreadable dirs / unstattable files warn and are skipped.\n */\nexport function discoverSourcemaps(\n dirs: string[],\n opts: { maxFiles?: number } = {},\n): DiscoveryResult {\n const maxFiles = opts.maxFiles ?? MAX_FILES;\n const warnings: string[] = [];\n const seen = new Map<string, string>(); // basename -> first absolute path\n const files: DiscoveredFile[] = [];\n let duplicatesDropped = 0;\n\n const walk = (dir: string): void => {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n warnings.push(`could not read directory: ${dir}`);\n return;\n }\n // Sort for deterministic ordering so \"keep the first\" is stable.\n entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\n for (const entry of entries) {\n if (entry.name === 'node_modules') continue;\n const full = join(dir, entry.name);\n\n if (entry.isDirectory()) {\n walk(full);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.map')) continue;\n\n const name = entry.name;\n const existing = seen.get(name);\n if (existing !== undefined) {\n duplicatesDropped++;\n warnings.push(\n `DUPLICATE sourcemap basename \"${name}\" — the server identifies maps by ` +\n `basename, so only ${existing} will be uploaded; ${full} is SKIPPED and ` +\n `its bundle will have NO sourcemap. Rename the bundle/map to disambiguate.`,\n );\n continue;\n }\n\n let byteSize: number;\n try {\n byteSize = statSync(full).size;\n } catch {\n warnings.push(`could not stat file: ${full}`);\n continue;\n }\n\n seen.set(name, full);\n files.push({ name, byteSize, absolutePath: full });\n }\n };\n\n for (const dir of dirs) walk(resolve(dir));\n\n // Prominent tally so a single dropped map isn't lost in the per-file noise.\n if (duplicatesDropped > 0) {\n warnings.push(\n `${duplicatesDropped} sourcemap(s) SKIPPED due to duplicate basenames — those ` +\n `bundles will have no sourcemap on the server. See the warnings above.`,\n );\n }\n\n let truncated = false;\n let result = files;\n if (files.length > maxFiles) {\n truncated = true;\n warnings.push(\n `found ${files.length} sourcemaps; capping at ${maxFiles} — the rest will be skipped.`,\n );\n result = files.slice(0, maxFiles);\n }\n\n return { files: result, warnings, truncated, duplicatesDropped };\n}\n\n/** Format a byte count as a short human string, e.g. `3.4 MB`, `512 B`. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '0 B';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unitIndex = 0;\n while (value >= 1024 && unitIndex < units.length - 1) {\n value /= 1024;\n unitIndex++;\n }\n return `${value.toFixed(1)} ${units[unitIndex]!}`;\n}\n"],"mappings":"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","names":["import_node_fs"]}
|
package/dist/index.cjs
CHANGED
|
@@ -29111,7 +29111,7 @@ async function domToBlob(node2, options) {
|
|
|
29111
29111
|
|
|
29112
29112
|
// ../core/dist/index.js
|
|
29113
29113
|
var SDK_NAME = "@merittdev/horus-lens";
|
|
29114
|
-
var SDK_VERSION = true ? "0.
|
|
29114
|
+
var SDK_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
29115
29115
|
var DEFAULT_ENDPOINT = "https://api.horus.sh";
|
|
29116
29116
|
var DEFAULT_REPLAY_WINDOW_MS = 15e3;
|
|
29117
29117
|
var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
|
|
@@ -29172,11 +29172,15 @@ function startAutoCapture(client, options = {}) {
|
|
|
29172
29172
|
const last = lastSeen.get(signature);
|
|
29173
29173
|
if (last !== void 0 && now - last < AUTO_CAPTURE_COOLDOWN_MS) return;
|
|
29174
29174
|
if (sessionCount >= AUTO_CAPTURE_SESSION_CAP) return;
|
|
29175
|
+
const previousSeen = lastSeen.get(signature);
|
|
29175
29176
|
lastSeen.set(signature, now);
|
|
29176
29177
|
sessionCount += 1;
|
|
29177
29178
|
submitting = true;
|
|
29178
29179
|
options.onCapture?.(signature);
|
|
29179
29180
|
void Promise.resolve().then(() => client.submitReport({ trigger: "error", include: { screenshot: false } })).catch(() => {
|
|
29181
|
+
if (previousSeen === void 0) lastSeen.delete(signature);
|
|
29182
|
+
else lastSeen.set(signature, previousSeen);
|
|
29183
|
+
sessionCount -= 1;
|
|
29180
29184
|
}).finally(() => {
|
|
29181
29185
|
submitting = false;
|
|
29182
29186
|
});
|
|
@@ -29231,6 +29235,14 @@ var RingBuffer = class {
|
|
|
29231
29235
|
return this.items.length;
|
|
29232
29236
|
}
|
|
29233
29237
|
};
|
|
29238
|
+
var HttpError = class extends Error {
|
|
29239
|
+
constructor(status, message) {
|
|
29240
|
+
super(message);
|
|
29241
|
+
this.status = status;
|
|
29242
|
+
this.name = "HttpError";
|
|
29243
|
+
}
|
|
29244
|
+
status;
|
|
29245
|
+
};
|
|
29234
29246
|
var Transport = class {
|
|
29235
29247
|
constructor(config2) {
|
|
29236
29248
|
this.config = config2;
|
|
@@ -29238,18 +29250,23 @@ var Transport = class {
|
|
|
29238
29250
|
config;
|
|
29239
29251
|
async initiate(assets) {
|
|
29240
29252
|
const body = { accessId: this.config.accessId, assets };
|
|
29241
|
-
return this.postJson(
|
|
29253
|
+
return this.postJson(
|
|
29254
|
+
`${this.config.endpoint}/v1/reports/initiate`,
|
|
29255
|
+
body,
|
|
29256
|
+
{ retries: 0 }
|
|
29257
|
+
);
|
|
29242
29258
|
}
|
|
29243
29259
|
/** Record a late-uploaded asset (background screenshot) on a finalized report. */
|
|
29244
29260
|
async attachAsset(reportId, asset) {
|
|
29245
29261
|
const body = { accessId: this.config.accessId, ...asset };
|
|
29246
29262
|
await this.postJson(`${this.config.endpoint}/v1/reports/${encodeURIComponent(reportId)}/assets`, body);
|
|
29247
29263
|
}
|
|
29248
|
-
async uploadAsset(upload, blob2) {
|
|
29264
|
+
async uploadAsset(upload, blob2, signal) {
|
|
29249
29265
|
const response = await fetch(upload.url, {
|
|
29250
29266
|
method: upload.method,
|
|
29251
29267
|
headers: upload.headers,
|
|
29252
|
-
body: blob2
|
|
29268
|
+
body: blob2,
|
|
29269
|
+
signal
|
|
29253
29270
|
});
|
|
29254
29271
|
if (!response.ok) {
|
|
29255
29272
|
throw new Error(`Asset upload failed (${upload.kind}): ${response.status}`);
|
|
@@ -29257,12 +29274,20 @@ var Transport = class {
|
|
|
29257
29274
|
}
|
|
29258
29275
|
async finalize(reportId, metadata, uploadedAssets) {
|
|
29259
29276
|
const body = { accessId: this.config.accessId, metadata, uploadedAssets };
|
|
29260
|
-
|
|
29261
|
-
|
|
29262
|
-
|
|
29263
|
-
|
|
29277
|
+
try {
|
|
29278
|
+
return await this.postJson(
|
|
29279
|
+
`${this.config.endpoint}/v1/reports/${reportId}/finalize`,
|
|
29280
|
+
body
|
|
29281
|
+
);
|
|
29282
|
+
} catch (err) {
|
|
29283
|
+
if (err instanceof HttpError && err.status === 409) {
|
|
29284
|
+
return { reportId, status: "submitted" };
|
|
29285
|
+
}
|
|
29286
|
+
throw err;
|
|
29287
|
+
}
|
|
29264
29288
|
}
|
|
29265
|
-
async postJson(url2, body,
|
|
29289
|
+
async postJson(url2, body, opts = {}) {
|
|
29290
|
+
const retries = opts.retries ?? 1;
|
|
29266
29291
|
let lastError;
|
|
29267
29292
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
29268
29293
|
try {
|
|
@@ -29273,7 +29298,10 @@ var Transport = class {
|
|
|
29273
29298
|
});
|
|
29274
29299
|
if (!response.ok) {
|
|
29275
29300
|
const text = await response.text().catch(() => "");
|
|
29276
|
-
throw new
|
|
29301
|
+
throw new HttpError(
|
|
29302
|
+
response.status,
|
|
29303
|
+
`Request to ${url2} failed: ${response.status} ${text}`.trim()
|
|
29304
|
+
);
|
|
29277
29305
|
}
|
|
29278
29306
|
return await response.json();
|
|
29279
29307
|
} catch (err) {
|
|
@@ -29714,8 +29742,14 @@ async function captureScreenshot(excludeHost, crop, composite) {
|
|
|
29714
29742
|
return null;
|
|
29715
29743
|
}
|
|
29716
29744
|
}
|
|
29717
|
-
|
|
29718
|
-
|
|
29745
|
+
const effectiveCrop = crop ?? (composite ? void 0 : {
|
|
29746
|
+
x: window.scrollX,
|
|
29747
|
+
y: window.scrollY,
|
|
29748
|
+
width: window.innerWidth,
|
|
29749
|
+
height: window.innerHeight
|
|
29750
|
+
});
|
|
29751
|
+
if (effectiveCrop) {
|
|
29752
|
+
const cropped = await cropBlob(blob2, effectiveCrop, scale);
|
|
29719
29753
|
return cropped ?? blob2;
|
|
29720
29754
|
}
|
|
29721
29755
|
return blob2;
|
|
@@ -30149,12 +30183,22 @@ var LensClient = class {
|
|
|
30149
30183
|
input2.screenshotCrop,
|
|
30150
30184
|
input2.annotationComposite
|
|
30151
30185
|
) : Promise.resolve(null);
|
|
30186
|
+
const consumingManualRecording = wantReplay && (this.replay?.hasManualRecording() ?? false) && !(this.replay?.isManualRecording() ?? false);
|
|
30152
30187
|
const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
|
|
30153
30188
|
const errorEntries = this.errorBuffer.drain();
|
|
30154
|
-
const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
|
|
30155
|
-
const networkTail = this.networkBuffer.tail(NETWORK_TAIL_SIZE);
|
|
30156
30189
|
const consoleEntries = this.consoleBuffer.drain();
|
|
30157
30190
|
const networkEntries = this.networkBuffer.drain();
|
|
30191
|
+
this.errorBuffer.clear();
|
|
30192
|
+
this.consoleBuffer.clear();
|
|
30193
|
+
this.networkBuffer.clear();
|
|
30194
|
+
const consoleTail = consoleEntries.slice(-CONSOLE_TAIL_SIZE);
|
|
30195
|
+
const isNetFailure = (e) => e.ok !== true || Boolean(e.error);
|
|
30196
|
+
const recentFailures = networkEntries.filter(isNetFailure).slice(-NETWORK_TAIL_SIZE);
|
|
30197
|
+
const keep = /* @__PURE__ */ new Set([
|
|
30198
|
+
...recentFailures,
|
|
30199
|
+
...networkEntries.slice(-NETWORK_TAIL_SIZE)
|
|
30200
|
+
]);
|
|
30201
|
+
const networkTail = networkEntries.filter((e) => keep.has(e)).slice(-NETWORK_TAIL_SIZE);
|
|
30158
30202
|
let metadata = {
|
|
30159
30203
|
annotations: input2.annotations ?? [],
|
|
30160
30204
|
env: collectEnv(),
|
|
@@ -30208,13 +30252,14 @@ var LensClient = class {
|
|
|
30208
30252
|
}
|
|
30209
30253
|
if (!asset.blob) continue;
|
|
30210
30254
|
try {
|
|
30211
|
-
await this.
|
|
30255
|
+
await this.uploadAssetWithTimeout(upload, asset.blob);
|
|
30212
30256
|
uploadedAssets.push({ kind: upload.kind, key: upload.key, byteSize: asset.blob.size });
|
|
30213
30257
|
} catch (err) {
|
|
30214
30258
|
this.warn(`asset upload failed (${upload.kind})`, err);
|
|
30215
30259
|
}
|
|
30216
30260
|
}
|
|
30217
30261
|
const finalized = await this.transport.finalize(initiated.reportId, metadata, uploadedAssets);
|
|
30262
|
+
if (consumingManualRecording) this.replay?.discardManualRecording();
|
|
30218
30263
|
if (screenshotUpload) {
|
|
30219
30264
|
const upload = screenshotUpload;
|
|
30220
30265
|
void screenshotPromise.then(async (blob2) => {
|
|
@@ -30232,6 +30277,21 @@ var LensClient = class {
|
|
|
30232
30277
|
}
|
|
30233
30278
|
return { reportId: finalized.reportId };
|
|
30234
30279
|
}
|
|
30280
|
+
/**
|
|
30281
|
+
* Upload a foreground asset with a bounded timeout. Without this a stuck S3
|
|
30282
|
+
* PUT (dropped connection, half-open socket) would hang `submitReport`
|
|
30283
|
+
* forever; the timeout aborts the individual PUT so it fails as one asset
|
|
30284
|
+
* (caught by the caller) instead of wedging the whole submit.
|
|
30285
|
+
*/
|
|
30286
|
+
async uploadAssetWithTimeout(upload, blob2) {
|
|
30287
|
+
const controller = new AbortController();
|
|
30288
|
+
const timer = setTimeout(() => controller.abort(), SCREENSHOT_TIMEOUT_MS);
|
|
30289
|
+
try {
|
|
30290
|
+
await this.transport.uploadAsset(upload, blob2, controller.signal);
|
|
30291
|
+
} finally {
|
|
30292
|
+
clearTimeout(timer);
|
|
30293
|
+
}
|
|
30294
|
+
}
|
|
30235
30295
|
warn(message, err) {
|
|
30236
30296
|
if (!this.debug) return;
|
|
30237
30297
|
if (err !== void 0) console.warn(`[lens] ${message}`, err);
|