@humanjs/playwright 0.3.0 → 0.4.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/index.js CHANGED
@@ -1,5 +1,12 @@
1
- import { resolvePersonality, createRng, planScroll, countWords, computeReadingDwellMs, planReadingScan, planTypeKeystrokes, bezierPath, humanizePath } from '@humanjs/core';
2
- export { applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality } from '@humanjs/core';
1
+ import { resolvePersonality, createRng, sleep as sleep$1, planScroll, countWords, computeReadingDwellMs, planReadingScan, planTypeKeystrokes, bezierPath, humanizePath } from '@humanjs/core';
2
+ export { applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality, sleep } from '@humanjs/core';
3
+ import { spawn } from 'child_process';
4
+ import { rmSync } from 'fs';
5
+ import { mkdir, writeFile, mkdtemp, rm } from 'fs/promises';
6
+ import { dirname, extname, join } from 'path';
7
+ import ffmpegStatic from 'ffmpeg-static';
8
+ import { tmpdir } from 'os';
9
+ export { chromium, firefox, webkit } from 'playwright';
3
10
 
4
11
  // src/index.ts
5
12
 
@@ -172,7 +179,8 @@ async function executeRead(target, ctx, options = {}) {
172
179
  personalitySpeed: ctx.personality.speed,
173
180
  speedFactor: speedModeFactor(ctx.speed)
174
181
  });
175
- if (options.withMotion && locator && durationMs > 0) {
182
+ const withMotion = options.withMotion ?? true;
183
+ if (withMotion && locator && durationMs > 0) {
176
184
  const box = await locator.boundingBox().catch(() => null);
177
185
  if (box) {
178
186
  const lineRects = await getLineRects(locator).catch(() => []);
@@ -232,6 +240,396 @@ async function detectKindFromTag(locator) {
232
240
  if (tag === "pre" || tag === "code") return "code";
233
241
  return void 0;
234
242
  }
243
+ var pendingFrameCleanups = /* @__PURE__ */ new Set();
244
+ var exitHandlerInstalled = false;
245
+ function ensureExitHandler() {
246
+ if (exitHandlerInstalled) return;
247
+ exitHandlerInstalled = true;
248
+ process.on("exit", () => {
249
+ for (const dir of pendingFrameCleanups) {
250
+ try {
251
+ rmSync(dir, { recursive: true, force: true });
252
+ } catch {
253
+ }
254
+ }
255
+ pendingFrameCleanups.clear();
256
+ });
257
+ }
258
+ var FFMPEG_PATH = ffmpegStatic;
259
+ var QUALITY_PRESETS = {
260
+ fast: {
261
+ captureFormat: "jpeg",
262
+ captureJpegQuality: 85,
263
+ captureFps: 24,
264
+ crf: 23,
265
+ preset: "fast"
266
+ },
267
+ standard: {
268
+ captureFormat: "jpeg",
269
+ captureJpegQuality: 90,
270
+ captureFps: 30,
271
+ crf: 20,
272
+ preset: "fast"
273
+ },
274
+ high: {
275
+ captureFormat: "jpeg",
276
+ captureJpegQuality: 95,
277
+ captureFps: 30,
278
+ crf: 18,
279
+ preset: "slow",
280
+ // 'animation' suits screen content (large solid regions, sharp edges)
281
+ // better than 'film' which is tuned for live-action grain.
282
+ tune: "animation"
283
+ },
284
+ lossless: {
285
+ // PNG capture for perceptually lossless source frames. Temp files are
286
+ // 10-20× larger than JPEG; output mp4 still benefits from the extra
287
+ // headroom (no JPEG artifacts to preserve).
288
+ captureFormat: "png",
289
+ captureJpegQuality: 100,
290
+ captureFps: 30,
291
+ crf: 12,
292
+ preset: "veryslow",
293
+ tune: "animation"
294
+ }
295
+ };
296
+ function getCaptureSettingsForQuality(quality) {
297
+ const preset = QUALITY_PRESETS[quality];
298
+ return {
299
+ format: preset.captureFormat,
300
+ quality: preset.captureJpegQuality,
301
+ fps: preset.captureFps
302
+ };
303
+ }
304
+ var Recording = class {
305
+ #capture;
306
+ #windowStartMs;
307
+ #windowEndMs;
308
+ #timelineSource;
309
+ // Frames live on disk until `dispose()` is called. Exporters
310
+ // (`toVideo`, `toGif`) are repeatable and interleavable — they read the
311
+ // same frame source, they don't consume it.
312
+ #disposed = false;
313
+ constructor(capture, windowStartMs, windowEndMs, timelineSource) {
314
+ this.#capture = capture;
315
+ this.#windowStartMs = windowStartMs;
316
+ this.#windowEndMs = windowEndMs;
317
+ this.#timelineSource = timelineSource;
318
+ if (capture !== null) {
319
+ pendingFrameCleanups.add(capture.dir);
320
+ ensureExitHandler();
321
+ }
322
+ }
323
+ /** Wall-clock duration of the recorded window. */
324
+ get durationMs() {
325
+ return this.#windowEndMs - this.#windowStartMs;
326
+ }
327
+ /** True if frames were captured during this recording. */
328
+ get hasVideo() {
329
+ return this.#capture !== null;
330
+ }
331
+ /**
332
+ * The structured action timeline of this recording — same data that
333
+ * `toTimeline()` writes to disk.
334
+ */
335
+ get timeline() {
336
+ return {
337
+ version: 1,
338
+ personality: this.#timelineSource.personality,
339
+ seed: this.#timelineSource.seed,
340
+ speed: this.#timelineSource.speed,
341
+ durationMs: this.durationMs,
342
+ events: this.#timelineSource.events
343
+ };
344
+ }
345
+ /**
346
+ * Assembles the captured frames into a video at `outputPath`. The output
347
+ * format is inferred from the extension — `.mp4` (H.264, re-encoded
348
+ * with the configured quality) or `.webm` (VP9).
349
+ *
350
+ * Repeatable and interleavable with `toGif()` — the frame source is read,
351
+ * not consumed. Frames live until you call `dispose()` (or `await using`
352
+ * goes out of scope, or the process exits and the OS reaps `tmpdir`).
353
+ *
354
+ * @returns the resolved output path.
355
+ */
356
+ async toVideo(outputPath, options = {}) {
357
+ if (this.#disposed) {
358
+ throw new Error("Recording.toVideo() called after dispose() \u2014 the source frames are gone.");
359
+ }
360
+ if (this.#capture === null) {
361
+ throw new Error(
362
+ "Recording.toVideo() requires video capture, which was disabled for this recording. Call `human.record(cb)` (default captures video) or pass `output` to @humanjs/recorder's `record()`. `toTimeline()` and `.timeline` work without capture."
363
+ );
364
+ }
365
+ const preset = QUALITY_PRESETS[options.quality ?? "high"];
366
+ const crf = options.crf ?? preset.crf;
367
+ const ffmpegPreset = options.preset ?? preset.preset;
368
+ const tune = options.tune ?? preset.tune;
369
+ const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
370
+ if (frames.length === 0) {
371
+ throw new Error(
372
+ "No frames were captured. The recording window may have been too short, or the page may not have rendered any frames before the callback completed."
373
+ );
374
+ }
375
+ await mkdir(dirname(outputPath), { recursive: true });
376
+ const ext = extname(outputPath).toLowerCase();
377
+ if (ext !== ".mp4" && ext !== ".webm") {
378
+ throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .mp4 or .webm.`);
379
+ }
380
+ const concatPath = `${dir}/concat.txt`;
381
+ const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
382
+ await writeFile(concatPath, concatBody, "utf8");
383
+ const args = ["-y", "-f", "concat", "-safe", "0", "-i", concatPath, "-vsync", "vfr"];
384
+ if (ext === ".mp4") {
385
+ args.push(
386
+ "-c:v",
387
+ "libx264",
388
+ "-pix_fmt",
389
+ "yuv420p",
390
+ "-crf",
391
+ String(crf),
392
+ "-preset",
393
+ ffmpegPreset
394
+ );
395
+ if (tune) args.push("-tune", tune);
396
+ args.push("-movflags", "+faststart");
397
+ } else {
398
+ args.push(
399
+ "-c:v",
400
+ "libvpx-vp9",
401
+ "-pix_fmt",
402
+ "yuv420p",
403
+ "-crf",
404
+ String(crf),
405
+ "-b:v",
406
+ "0",
407
+ "-deadline",
408
+ ffmpegPreset === "fast" || ffmpegPreset === "veryfast" ? "realtime" : "good"
409
+ );
410
+ }
411
+ args.push(outputPath);
412
+ await runFfmpeg(args);
413
+ return outputPath;
414
+ }
415
+ /**
416
+ * Assembles the captured frames into an animated GIF at `outputPath`.
417
+ * Optimized for embedding in READMEs, PRs, Slack, and docs — uses a
418
+ * per-recording palette (`palettegen` + `paletteuse`) with Bayer dithering
419
+ * so gradients stay smooth without exploding the file size.
420
+ *
421
+ * Repeatable and interleavable with `toVideo()` — call them in any order,
422
+ * any number of times. Frames live until you call `dispose()`.
423
+ *
424
+ * @returns the resolved output path.
425
+ */
426
+ async toGif(outputPath, options = {}) {
427
+ if (this.#disposed) {
428
+ throw new Error("Recording.toGif() called after dispose() \u2014 the source frames are gone.");
429
+ }
430
+ if (this.#capture === null) {
431
+ throw new Error(
432
+ "Recording.toGif() requires video capture, which was disabled for this recording. Call `human.record(cb)` (default captures video) or pass `output` to @humanjs/recorder's `record()`. `toTimeline()` and `.timeline` work without capture."
433
+ );
434
+ }
435
+ const fps = options.fps ?? 15;
436
+ const width = options.width;
437
+ const { dir, frames, startedAtMs, stoppedAtMs } = this.#capture;
438
+ if (frames.length === 0) {
439
+ throw new Error(
440
+ "No frames were captured. The recording window may have been too short, or the page may not have rendered any frames before the callback completed."
441
+ );
442
+ }
443
+ await mkdir(dirname(outputPath), { recursive: true });
444
+ const ext = extname(outputPath).toLowerCase();
445
+ if (ext !== ".gif") {
446
+ throw new Error(`Unsupported output extension: ${ext || "(none)"}. Use .gif.`);
447
+ }
448
+ const concatPath = `${dir}/concat.txt`;
449
+ const concatBody = buildConcatFile(frames, stoppedAtMs - startedAtMs);
450
+ await writeFile(concatPath, concatBody, "utf8");
451
+ const filterSteps = [`fps=${fps}`];
452
+ if (width !== void 0) {
453
+ filterSteps.push(`scale=${width}:-1:flags=lanczos`);
454
+ }
455
+ const preFilter = filterSteps.join(",");
456
+ const filterComplex = `${preFilter},split [a][b]; [a] palettegen=stats_mode=diff [p]; [b][p] paletteuse=dither=bayer:bayer_scale=5`;
457
+ const args = [
458
+ "-y",
459
+ "-f",
460
+ "concat",
461
+ "-safe",
462
+ "0",
463
+ "-i",
464
+ concatPath,
465
+ "-filter_complex",
466
+ filterComplex,
467
+ "-loop",
468
+ "0",
469
+ outputPath
470
+ ];
471
+ await runFfmpeg(args);
472
+ return outputPath;
473
+ }
474
+ /**
475
+ * Writes the structured action timeline to `outputPath` as JSON.
476
+ * Independent of `toVideo()` / `toGif()` — call before, after, in between,
477
+ * or instead. Safe to call multiple times. Unaffected by `dispose()`
478
+ * (the timeline lives in memory, not in the captured-frames temp dir).
479
+ *
480
+ * @returns the resolved output path.
481
+ */
482
+ async toTimeline(outputPath) {
483
+ await mkdir(dirname(outputPath), { recursive: true });
484
+ await writeFile(outputPath, `${JSON.stringify(this.timeline, null, 2)}
485
+ `, "utf8");
486
+ return outputPath;
487
+ }
488
+ /**
489
+ * Releases the captured-frames temp directory. After this call, `toVideo()`
490
+ * and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
491
+ * still work because those don't depend on the frames.
492
+ *
493
+ * **Optional.** A process-exit handler also sweeps any un-disposed frame
494
+ * dirs, so casual scripts can skip this entirely. Call it explicitly when
495
+ * you want to release frames proactively (long-running services, batch
496
+ * jobs, or anywhere you want predictable disk usage).
497
+ *
498
+ * Idempotent. Safe to call on a Recording that never had a capture
499
+ * (timeline-only mode) — no-op there.
500
+ *
501
+ * Also wired to `Symbol.asyncDispose`, so the explicit-resource-management
502
+ * `await using` syntax (TypeScript ≥ 5.2 / Node ≥ 20.4) works:
503
+ *
504
+ * ```ts
505
+ * await using rec = await human.record(fn);
506
+ * await rec.toVideo('demo.mp4');
507
+ * await rec.toGif('demo.gif');
508
+ * // frames cleaned up automatically when `rec` goes out of scope
509
+ * ```
510
+ */
511
+ async dispose() {
512
+ if (this.#disposed) return;
513
+ if (this.#capture !== null) {
514
+ await this.#capture.cleanup();
515
+ pendingFrameCleanups.delete(this.#capture.dir);
516
+ }
517
+ this.#disposed = true;
518
+ }
519
+ async [Symbol.asyncDispose]() {
520
+ await this.dispose();
521
+ }
522
+ };
523
+ function buildConcatFile(frames, totalMs) {
524
+ const lines = [];
525
+ for (let i = 0; i < frames.length; i++) {
526
+ const frame = frames[i];
527
+ const next = frames[i + 1];
528
+ const nextTMs = next ? next.tMs : totalMs;
529
+ const durationS = Math.max(1e-3, (nextTMs - frame.tMs) / 1e3);
530
+ lines.push(`file '${frame.path.replaceAll("'", "'\\''")}'`);
531
+ lines.push(`duration ${durationS.toFixed(6)}`);
532
+ }
533
+ const last = frames[frames.length - 1];
534
+ if (last) {
535
+ lines.push(`file '${last.path.replaceAll("'", "'\\''")}'`);
536
+ }
537
+ return `${lines.join("\n")}
538
+ `;
539
+ }
540
+ function runFfmpeg(args) {
541
+ if (!FFMPEG_PATH) {
542
+ return Promise.reject(
543
+ new Error(
544
+ "ffmpeg-static did not bundle a binary for this platform. Install system ffmpeg and set FFMPEG_PATH, or run on a supported platform."
545
+ )
546
+ );
547
+ }
548
+ return new Promise((resolve, reject) => {
549
+ const proc = spawn(FFMPEG_PATH, [...args]);
550
+ let stderr = "";
551
+ proc.stderr?.on("data", (chunk) => {
552
+ stderr += chunk.toString();
553
+ });
554
+ proc.on("error", reject);
555
+ proc.on("close", (code) => {
556
+ if (code === 0) resolve();
557
+ else reject(new Error(`ffmpeg exited with code ${code}
558
+ ${stderr.trim()}`));
559
+ });
560
+ });
561
+ }
562
+ async function startCapture(page, options = {}) {
563
+ const format = options.format ?? "jpeg";
564
+ const quality = options.quality ?? 95;
565
+ const fps = Math.max(1, Math.min(60, options.fps ?? 30));
566
+ const intervalMs = 1e3 / fps;
567
+ const dir = await mkdtemp(join(tmpdir(), "humanjs-capture-"));
568
+ const frames = [];
569
+ const ext = format === "png" ? "png" : "jpg";
570
+ let stopped = false;
571
+ let frameIndex = 0;
572
+ const writes = [];
573
+ const startedAtMs = Date.now();
574
+ const captureLoop = async () => {
575
+ while (!stopped) {
576
+ const loopStart = Date.now();
577
+ try {
578
+ const buf = await page.screenshot({
579
+ type: format,
580
+ quality: format === "jpeg" ? quality : void 0
581
+ });
582
+ if (stopped) return;
583
+ const idx = frameIndex++;
584
+ const path = join(dir, `frame_${String(idx).padStart(6, "0")}.${ext}`);
585
+ const tMs = loopStart - startedAtMs;
586
+ writes.push(
587
+ writeFile(path, buf).then(
588
+ () => {
589
+ frames.push({ path, tMs });
590
+ },
591
+ (err) => {
592
+ console.warn(`humanjs capture: write failed for frame ${idx}:`, err);
593
+ }
594
+ )
595
+ );
596
+ } catch (err) {
597
+ if (stopped) return;
598
+ console.warn("humanjs capture: screenshot failed, stopping loop:", err);
599
+ stopped = true;
600
+ return;
601
+ }
602
+ const elapsed = Date.now() - loopStart;
603
+ const wait = intervalMs - elapsed;
604
+ if (wait > 0) await sleep$1(wait);
605
+ }
606
+ };
607
+ const loopPromise = captureLoop();
608
+ const finish = async () => {
609
+ stopped = true;
610
+ await loopPromise;
611
+ await Promise.allSettled(writes);
612
+ };
613
+ return {
614
+ async stop() {
615
+ await finish();
616
+ const stoppedAtMs = Date.now();
617
+ return {
618
+ dir,
619
+ frames: [...frames].sort((a, b) => a.tMs - b.tMs),
620
+ startedAtMs,
621
+ stoppedAtMs,
622
+ format,
623
+ fps,
624
+ cleanup: () => rm(dir, { recursive: true, force: true }).then(() => void 0)
625
+ };
626
+ },
627
+ async abort() {
628
+ await finish();
629
+ await rm(dir, { recursive: true, force: true }).catch(() => void 0);
630
+ }
631
+ };
632
+ }
235
633
  var RESERVED_TARGETS = /* @__PURE__ */ new Set(["natural", "end", "top"]);
236
634
  async function executeScroll(target, ctx, options = {}) {
237
635
  const { page, personality, rng, speed } = ctx;
@@ -407,7 +805,11 @@ function clamp2(value, min, max) {
407
805
 
408
806
  // src/mouse-helper/index.ts
409
807
  var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
808
+ var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
410
809
  async function installMouseHelper(target, options = {}) {
810
+ const tagged = target;
811
+ if (tagged[INSTALLED_FLAG]) return;
812
+ tagged[INSTALLED_FLAG] = true;
411
813
  const config = {
412
814
  color: options.color ?? "#f5a55c",
413
815
  stroke: "#020203",
@@ -417,16 +819,22 @@ async function installMouseHelper(target, options = {}) {
417
819
  path: CURSOR_PATH
418
820
  };
419
821
  await target.addInitScript(installScript, config);
822
+ const attachPageHooks = (page) => {
823
+ page.on("domcontentloaded", () => {
824
+ page.evaluate(installScript, config).catch(() => void 0);
825
+ });
826
+ };
420
827
  const pages = "pages" in target ? target.pages() : [target];
828
+ for (const page of pages) attachPageHooks(page);
829
+ if ("on" in target && "newPage" in target) {
830
+ target.on("page", attachPageHooks);
831
+ }
421
832
  await Promise.all(
422
833
  pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
423
834
  );
424
835
  }
425
836
  function installScript(config) {
426
- const guardKey = "__humanjsMouseHelperInstalled";
427
- const w = window;
428
- if (w[guardKey]) return;
429
- w[guardKey] = true;
837
+ if (document.querySelector("[data-humanjs-cursor]")) return;
430
838
  const attach = () => {
431
839
  const cursor = document.createElement("div");
432
840
  cursor.setAttribute("aria-hidden", "true");
@@ -517,6 +925,9 @@ async function createHuman(page, options = {}) {
517
925
  for (const plugin of plugins) {
518
926
  await plugin.install?.(context);
519
927
  }
928
+ let hasRecorded = false;
929
+ let activeRecordingEvents = null;
930
+ let activeRecordingStartMs = 0;
520
931
  async function performAction(action, actionFn) {
521
932
  for (const plugin of plugins) {
522
933
  await plugin.beforeAction?.(action);
@@ -524,15 +935,30 @@ async function createHuman(page, options = {}) {
524
935
  const startedAt = Date.now();
525
936
  try {
526
937
  const value = await actionFn();
527
- const result = {
528
- type: action.type,
529
- durationMs: Date.now() - startedAt
530
- };
938
+ const durationMs = Date.now() - startedAt;
939
+ const result = { type: action.type, durationMs };
940
+ if (activeRecordingEvents !== null && action.type !== "record") {
941
+ activeRecordingEvents.push({
942
+ type: action.type,
943
+ params: action.params ?? {},
944
+ tMs: startedAt - activeRecordingStartMs,
945
+ durationMs
946
+ });
947
+ }
531
948
  for (const plugin of plugins) {
532
949
  await plugin.afterAction?.(action, result);
533
950
  }
534
951
  return value;
535
952
  } catch (error) {
953
+ if (activeRecordingEvents !== null && action.type !== "record") {
954
+ activeRecordingEvents.push({
955
+ type: action.type,
956
+ params: action.params ?? {},
957
+ tMs: startedAt - activeRecordingStartMs,
958
+ durationMs: Date.now() - startedAt,
959
+ error: error instanceof Error ? error.message : String(error)
960
+ });
961
+ }
536
962
  for (const plugin of plugins) {
537
963
  await plugin.onError?.(action, error);
538
964
  }
@@ -610,6 +1036,51 @@ async function createHuman(page, options = {}) {
610
1036
  },
611
1037
  () => executeScroll(target, { page, personality, rng, speed }, options2)
612
1038
  );
1039
+ },
1040
+ async sleep(ms) {
1041
+ await performAction({ type: "sleep", params: { ms } }, () => sleep$1(ms));
1042
+ },
1043
+ async record(optionsOrFn, maybeFn) {
1044
+ const [recordOptions, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
1045
+ if (hasRecorded) {
1046
+ throw new Error(
1047
+ "human.record() can only be called once per session. Create a new browser context (and a new human session) to record a separate clip."
1048
+ );
1049
+ }
1050
+ hasRecorded = true;
1051
+ const captureEnabled = recordOptions.video !== false;
1052
+ const captureQuality = recordOptions.quality ?? "high";
1053
+ let captureSession = null;
1054
+ if (captureEnabled) {
1055
+ const { format, quality, fps } = getCaptureSettingsForQuality(captureQuality);
1056
+ captureSession = await startCapture(page, { format, quality, fps });
1057
+ }
1058
+ const events = [];
1059
+ const windowStartMs = Date.now();
1060
+ activeRecordingEvents = events;
1061
+ activeRecordingStartMs = windowStartMs;
1062
+ let windowEndMs = windowStartMs;
1063
+ try {
1064
+ await performAction({ type: "record", params: {} }, async () => {
1065
+ try {
1066
+ await fn();
1067
+ } finally {
1068
+ windowEndMs = Date.now();
1069
+ }
1070
+ });
1071
+ } catch (error) {
1072
+ if (captureSession) await captureSession.abort();
1073
+ throw error;
1074
+ } finally {
1075
+ activeRecordingEvents = null;
1076
+ }
1077
+ const captureResult = captureSession ? await captureSession.stop() : null;
1078
+ return new Recording(captureResult, windowStartMs, windowEndMs, {
1079
+ personality: personality.name,
1080
+ seed: options.seed === void 0 ? null : String(options.seed),
1081
+ speed,
1082
+ events
1083
+ });
613
1084
  }
614
1085
  };
615
1086
  }
@@ -629,6 +1100,6 @@ function describeReadTarget(target) {
629
1100
  return target.toString?.() ?? "locator";
630
1101
  }
631
1102
 
632
- export { createHuman, installMouseHelper };
1103
+ export { Recording, createHuman, installMouseHelper };
633
1104
  //# sourceMappingURL=index.js.map
634
1105
  //# sourceMappingURL=index.js.map