@playruo/embed 1.3.3 → 1.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/index.js CHANGED
@@ -8,27 +8,72 @@ var normalizeOperation = (operation) => {
8
8
  if (trimmedOperation === "") {
9
9
  throw new Error("`operation` is required to build a Playruo embed URL.");
10
10
  }
11
+ const operationQuery = trimmedOperation.split("?")[1];
12
+ if (operationQuery != null && new URLSearchParams(operationQuery).has("op")) {
13
+ console.warn(
14
+ "[PlayruoEmbed] `?op=` inside `operation` is deprecated: pass it as the `operationKey` prop."
15
+ );
16
+ }
11
17
  return trimmedOperation.startsWith("/") ? trimmedOperation.slice(1) : trimmedOperation;
12
18
  };
13
- var buildEmbedUrl = ({ operation, storeUrl }) => {
14
- return buildEmbedUrlWithOrigin(PLAYRUO_ORIGIN, { operation, storeUrl });
19
+ var buildEmbedUrl = ({
20
+ operation,
21
+ operationKey,
22
+ storeUrl
23
+ }) => {
24
+ return buildEmbedUrlWithOrigin(PLAYRUO_ORIGIN, {
25
+ operation,
26
+ operationKey,
27
+ storeUrl
28
+ });
15
29
  };
16
- var buildEmbedUrlWithOrigin = (origin, { operation, storeUrl }) => {
30
+ var buildEmbedUrlWithOrigin = (origin, { operation, operationKey, storeUrl }) => {
17
31
  const operationPath = normalizeOperation(operation);
18
32
  const url = new URL(`/${operationPath}`, origin);
19
33
  url.searchParams.set("iframe", "true");
34
+ const trimmedOperationKey = operationKey?.trim();
35
+ if (trimmedOperationKey) {
36
+ url.searchParams.set("op", trimmedOperationKey);
37
+ }
20
38
  if (storeUrl) {
21
39
  url.searchParams.set("storeUrl", storeUrl);
22
40
  }
23
41
  return url.toString();
24
42
  };
25
43
 
44
+ // src/visitor-uuid.ts
45
+ var PLAYRUO_UUID_STORAGE_KEY = "playruo_uuid";
46
+ var generateUUID = () => {
47
+ if (self.crypto.randomUUID) return self.crypto.randomUUID();
48
+ const bytes = new Uint8Array(16);
49
+ self.crypto.getRandomValues(bytes);
50
+ bytes[6] = bytes[6] & 15 | 64;
51
+ bytes[8] = bytes[8] & 63 | 128;
52
+ return [...bytes].map(
53
+ (b, i) => ([4, 6, 8, 10].includes(i) ? "-" : "") + b.toString(16).padStart(2, "0")
54
+ ).join("");
55
+ };
56
+ var resolveVisitorUuid = (clientFingerprint) => {
57
+ const provided = clientFingerprint?.trim();
58
+ if (provided) {
59
+ return provided;
60
+ }
61
+ let uuid = localStorage.getItem(PLAYRUO_UUID_STORAGE_KEY);
62
+ if (!uuid) {
63
+ uuid = generateUUID();
64
+ localStorage.setItem(PLAYRUO_UUID_STORAGE_KEY, uuid);
65
+ }
66
+ return uuid;
67
+ };
68
+
26
69
  // src/react/playruo-embed.tsx
27
70
  import { jsx, jsxs } from "react/jsx-runtime";
28
71
  var PlayruoEmbed = ({
29
72
  baseUrl,
30
73
  operation,
74
+ operationKey,
31
75
  storeUrl,
76
+ clientFingerprint,
32
77
  showIframe = false,
33
78
  className,
34
79
  onReady,
@@ -37,6 +82,8 @@ var PlayruoEmbed = ({
37
82
  onEvent,
38
83
  onPurchaseRequest,
39
84
  onError,
85
+ onPasswordRequired,
86
+ onPasswordResult,
40
87
  enableWindowedLaunch = false,
41
88
  forwardEventsToParent = true,
42
89
  renderState,
@@ -54,12 +101,24 @@ var PlayruoEmbed = ({
54
101
  const [isValidChromium, setIsValidChromium] = useState(true);
55
102
  const [errorMessage, setErrorMessage] = useState(null);
56
103
  const [iframeState, setIframeState] = useState(null);
104
+ const [passwordRequired, setPasswordRequired] = useState(false);
105
+ const [passwordChecked, setPasswordChecked] = useState(false);
106
+ const [passwordUnlocked, setPasswordUnlocked] = useState(false);
107
+ const [passwordError, setPasswordError] = useState(
108
+ void 0
109
+ );
57
110
  const src = useMemo(
58
- () => baseUrl != null && baseUrl !== "" ? buildEmbedUrlWithOrigin(baseUrl, { operation, storeUrl }) : buildEmbedUrl({ operation, storeUrl }),
59
- [baseUrl, operation, storeUrl]
111
+ () => baseUrl != null && baseUrl !== "" ? buildEmbedUrlWithOrigin(baseUrl, { operation, operationKey, storeUrl }) : buildEmbedUrl({ operation, operationKey, storeUrl }),
112
+ [baseUrl, operation, operationKey, storeUrl]
60
113
  );
114
+ const embedOrigin = useMemo(() => new URL(src).origin, [src]);
115
+ useEffect(() => {
116
+ setPasswordRequired(false);
117
+ setPasswordChecked(false);
118
+ setPasswordUnlocked(false);
119
+ setPasswordError(void 0);
120
+ }, [src]);
61
121
  useEffect(() => {
62
- if (typeof window === "undefined") return;
63
122
  const detectMobile = () => {
64
123
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
65
124
  return /android|iPad|iPhone|iPod/i.test(userAgent);
@@ -97,15 +156,14 @@ var PlayruoEmbed = ({
97
156
  tryNow: {
98
157
  ...currentState.tryNow,
99
158
  available: hasIframeConfirmed,
100
- status: currentState.tryNow.status ?? (hasIframeConfirmed ? "Ready" : isCompatible ? "Checking availability\u2026" : "Browser not compatible")
159
+ // A missing status means the state came from the iframe, which only loads on
160
+ // compatible browsers — the incompatible copy is supplied by currentState.
161
+ status: currentState.tryNow.status ?? (hasIframeConfirmed ? "Ready" : "Checking availability\u2026")
101
162
  }
102
163
  }),
103
- [currentState, hasIframeConfirmed, isCompatible]
164
+ [currentState, hasIframeConfirmed]
104
165
  );
105
166
  const getKeyboardApi = useCallback(() => {
106
- if (typeof navigator === "undefined") {
107
- return void 0;
108
- }
109
167
  return navigator.keyboard;
110
168
  }, []);
111
169
  const releaseEscapeKeyLock = useCallback(() => {
@@ -135,6 +193,23 @@ var PlayruoEmbed = ({
135
193
  },
136
194
  [onDemoCompleted, storeUrl]
137
195
  );
196
+ const postPasswordAction = useCallback(
197
+ (message) => {
198
+ const contentWindow = iframeRef.current?.contentWindow;
199
+ if (!contentWindow) {
200
+ return;
201
+ }
202
+ try {
203
+ contentWindow.postMessage(
204
+ { type: "playruo", fromEmbed: true, ...message },
205
+ embedOrigin
206
+ );
207
+ } catch {
208
+ console.error("[PlayruoEmbed] Failed to send the password action");
209
+ }
210
+ },
211
+ [embedOrigin, iframeRef]
212
+ );
138
213
  const postExitFullscreenToIframe = useCallback(() => {
139
214
  const iframe = iframeRef.current;
140
215
  if (!iframe) {
@@ -147,22 +222,22 @@ var PlayruoEmbed = ({
147
222
  fromEmbed: true,
148
223
  isFullscreen: false,
149
224
  parentWillHandleFullscreen: true
150
- }, "*");
225
+ }, embedOrigin);
151
226
  } catch {
152
227
  console.error("[PlayruoEmbed] Failed to exit fullscreen");
153
228
  }
154
- }, [iframeRef]);
229
+ }, [embedOrigin, iframeRef]);
155
230
  const postExitFullscreenAckToIframe = useCallback(() => {
156
231
  const iframe = iframeRef.current;
157
232
  if (!iframe) {
158
233
  return;
159
234
  }
160
235
  try {
161
- iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, "*");
236
+ iframe.contentWindow?.postMessage({ type: "ACK_EXIT_FULLSCREEN" }, embedOrigin);
162
237
  } catch {
163
238
  console.error("[PlayruoEmbed] Failed to acknowledge fullscreen exit");
164
239
  }
165
- }, [iframeRef]);
240
+ }, [embedOrigin, iframeRef]);
166
241
  const exitEmbedFullscreen = useCallback(
167
242
  async ({
168
243
  reason = "cleanup",
@@ -248,7 +323,7 @@ var PlayruoEmbed = ({
248
323
  }, []);
249
324
  const forwardEventToParent = useCallback(
250
325
  (message) => {
251
- if (!forwardEventsToParent || typeof window === "undefined" || window.parent === window) {
326
+ if (!forwardEventsToParent || window.parent === window) {
252
327
  return;
253
328
  }
254
329
  try {
@@ -317,7 +392,7 @@ var PlayruoEmbed = ({
317
392
  isFullscreen: isParentFullscreen,
318
393
  parentWillHandleFullscreen: isParentFullscreen,
319
394
  isReplay: hasPlayedOnce
320
- }, "*");
395
+ }, embedOrigin);
321
396
  console.log("[PlayruoEmbed] Sent tryNowClick to game", hasPlayedOnce ? "(replay)" : "(first time)");
322
397
  iframe.focus();
323
398
  requestAnimationFrame(() => {
@@ -364,6 +439,9 @@ var PlayruoEmbed = ({
364
439
  if (!iframe || event.source !== iframe.contentWindow) {
365
440
  return;
366
441
  }
442
+ if (event.origin !== embedOrigin) {
443
+ return;
444
+ }
367
445
  console.log("\u{1F537} [Embed] Message received from iframe:", { type, data });
368
446
  if (type === "playruo_ready") {
369
447
  console.log("\u{1F537} [Embed] playruo_ready received");
@@ -432,6 +510,24 @@ var PlayruoEmbed = ({
432
510
  const eventPayload = message.payload;
433
511
  onEvent?.(eventPayload);
434
512
  forwardEventToParent({ type: "playruo_event", data: eventPayload });
513
+ } else if (type === "playruo_password_required") {
514
+ console.log("\u{1F537} [Embed] playruo_password_required received");
515
+ setPasswordRequired(true);
516
+ setPasswordChecked(true);
517
+ setPasswordUnlocked(false);
518
+ setPasswordError(void 0);
519
+ onPasswordRequired?.();
520
+ } else if (type === "playruo_password_not_required") {
521
+ console.log("\u{1F537} [Embed] playruo_password_not_required received");
522
+ setPasswordRequired(false);
523
+ setPasswordChecked(true);
524
+ } else if (type === "playruo_password_result" && data) {
525
+ const result = data;
526
+ const success = result.success === true;
527
+ console.log("\u{1F537} [Embed] playruo_password_result received:", result);
528
+ setPasswordUnlocked(success);
529
+ setPasswordError(success ? void 0 : result.error ?? "unknown");
530
+ onPasswordResult?.({ success, error: result.error });
435
531
  }
436
532
  if (type === "error" || type === "closeForBuy" || type === "goToStore") {
437
533
  console.log("\u{1F537} [Embed] Legacy message received:", type);
@@ -447,6 +543,7 @@ var PlayruoEmbed = ({
447
543
  return () => window.removeEventListener("message", handleGameMessage);
448
544
  }, [
449
545
  cleanupEmbedSession,
546
+ embedOrigin,
450
547
  exitEmbedFullscreen,
451
548
  forwardEventToParent,
452
549
  hasPlayedOnce,
@@ -454,70 +551,53 @@ var PlayruoEmbed = ({
454
551
  markStreamActive,
455
552
  onEvent,
456
553
  onError,
554
+ onPasswordRequired,
555
+ onPasswordResult,
457
556
  onPurchaseRequest,
458
557
  onReady,
459
558
  onStateChange,
460
559
  postExitFullscreenAckToIframe,
461
560
  storeUrl
462
561
  ]);
463
- const generateUUID = () => {
464
- if (self.crypto.randomUUID) return self.crypto.randomUUID();
465
- const bytes = new Uint8Array(16);
466
- self.crypto.getRandomValues(bytes);
467
- bytes[6] = bytes[6] & 15 | 64;
468
- bytes[8] = bytes[8] & 63 | 128;
469
- return [...bytes].map(
470
- (b, i) => ([4, 6, 8, 10].includes(i) ? "-" : "") + b.toString(16).padStart(2, "0")
471
- ).join("");
472
- };
473
562
  useEffect(() => {
474
563
  const handleUUIDMessage = (event) => {
475
564
  const iframe = iframeRef.current;
476
565
  if (!iframe || event.source !== iframe.contentWindow) {
477
566
  return;
478
567
  }
568
+ if (event.origin !== embedOrigin) {
569
+ return;
570
+ }
479
571
  const { type, requestId } = event.data || {};
480
572
  if (type === "playruo_uuid_request") {
481
573
  try {
482
- let storedUUID = localStorage.getItem("playruo_uuid");
483
- if (!storedUUID) {
484
- storedUUID = generateUUID();
485
- localStorage.setItem("playruo_uuid", storedUUID);
486
- }
487
574
  iframe.contentWindow?.postMessage(
488
575
  {
489
576
  type: "playruo_uuid_response",
490
577
  requestId,
491
- uuid: storedUUID
578
+ uuid: resolveVisitorUuid(clientFingerprint)
492
579
  },
493
- "*"
580
+ embedOrigin
494
581
  );
495
582
  } catch (e) {
496
583
  }
497
- } else if (type === "playruo_uuid_remove") {
498
- try {
499
- localStorage.removeItem("playruo_uuid");
500
- } catch (e) {
501
- }
502
584
  }
503
585
  };
504
586
  window.addEventListener("message", handleUUIDMessage);
505
587
  return () => window.removeEventListener("message", handleUUIDMessage);
506
- }, [iframeRef]);
588
+ }, [clientFingerprint, embedOrigin, iframeRef]);
507
589
  useEffect(() => {
508
590
  const handleFullscreenChange = () => {
509
591
  const iframe = iframeRef.current;
510
592
  const fullscreenElement = getFullscreenElement();
511
593
  const isEmbedFullscreen = Boolean(iframe && fullscreenElement === iframe);
512
- if (isEmbedFullscreen) {
594
+ if (isEmbedFullscreen && iframe) {
513
595
  wasEmbedFullscreenRef.current = true;
514
596
  void lockEscapeKey();
515
- if (iframe) {
516
- setTimeout(() => {
517
- iframe.focus();
518
- iframe.contentWindow?.focus();
519
- }, 200);
520
- }
597
+ setTimeout(() => {
598
+ iframe.focus();
599
+ iframe.contentWindow?.focus();
600
+ }, 200);
521
601
  return;
522
602
  }
523
603
  if (fullscreenElement) {
@@ -562,17 +642,16 @@ var PlayruoEmbed = ({
562
642
  const logCrossOriginStatus = () => {
563
643
  const isIsolated = self.crossOriginIsolated;
564
644
  const hasSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined";
565
- const hasHighResTime = typeof performance !== "undefined" && typeof performance.now === "function";
566
645
  console.group("[PlayruoEmbed] \u{1F512} Cross-Origin Isolation Status");
567
646
  if (isIsolated) {
568
647
  console.log("\u2705 Cross-origin isolation: ENABLED");
569
648
  console.log("\u2705 SharedArrayBuffer support:", hasSharedArrayBuffer ? "Available" : "Not available");
570
- console.log("\u2705 High-resolution timers:", hasHighResTime ? "Available" : "Not available");
649
+ console.log("\u2705 High-resolution timers: Full precision");
571
650
  console.log("\u{1F3AE} Game performance: Optimal");
572
651
  } else {
573
652
  console.warn("\u26A0\uFE0F Cross-origin isolation: DISABLED");
574
653
  console.warn("\u26A0\uFE0F SharedArrayBuffer support:", hasSharedArrayBuffer ? "Available" : "Not available");
575
- console.warn("\u26A0\uFE0F High-resolution timers:", hasHighResTime ? "Available" : "Not available");
654
+ console.warn("\u26A0\uFE0F High-resolution timers: Reduced precision");
576
655
  console.warn("\u{1F3AE} Game performance: May be degraded");
577
656
  console.group("\u{1F4CB} Setup Instructions:");
578
657
  console.log("Add these headers to your server:");
@@ -616,7 +695,7 @@ var PlayruoEmbed = ({
616
695
  console.log("User agent:", navigator.userAgent);
617
696
  console.log("Available APIs:", {
618
697
  SharedArrayBuffer: hasSharedArrayBuffer,
619
- Performance: hasHighResTime,
698
+ Performance: true,
620
699
  WebGL: !!document.createElement("canvas").getContext("webgl"),
621
700
  WebGL2: !!document.createElement("canvas").getContext("webgl2"),
622
701
  WebAssembly: typeof WebAssembly !== "undefined"
@@ -648,22 +727,44 @@ var PlayruoEmbed = ({
648
727
  tryNow: handleTryNow,
649
728
  buyNow: handleBuyNow,
650
729
  canTryNow,
651
- canBuyNow: Boolean(isReady)
730
+ canBuyNow: Boolean(isReady),
731
+ submitPassword: (password2) => {
732
+ if (!password2) {
733
+ return;
734
+ }
735
+ postPasswordAction({ action: "submitPassword", password: password2 });
736
+ },
737
+ requestPassword: () => postPasswordAction({ action: "requestPassword" })
738
+ };
739
+ const password = {
740
+ required: passwordRequired,
741
+ checked: passwordChecked,
742
+ unlocked: passwordUnlocked,
743
+ error: passwordError
652
744
  };
653
745
  if (errorMessage) {
654
- return renderState({ status: "error", message: errorMessage, actions });
746
+ return renderState({ status: "error", message: errorMessage, actions, password });
747
+ }
748
+ if (password.required && !password.unlocked) {
749
+ return renderState({
750
+ status: "loading",
751
+ message: "Password required",
752
+ actions,
753
+ password
754
+ });
655
755
  }
656
756
  if (!isReady) {
657
- return renderState({ status: "loading", message: "Loading\u2026", actions });
757
+ return renderState({ status: "loading", message: "Loading\u2026", actions, password });
658
758
  }
659
759
  if (isCompatible && !iframeState) {
660
- return renderState({ status: "loading", message: "Checking availability\u2026", actions });
760
+ return renderState({ status: "loading", message: "Checking availability\u2026", actions, password });
661
761
  }
662
762
  const message = !isAvailable ? currentState.tryNow.status ?? exposedState.tryNow.status : void 0;
663
763
  return renderState({
664
764
  status: isAvailable ? "available" : "unavailable",
665
765
  message,
666
- actions
766
+ actions,
767
+ password
667
768
  });
668
769
  };
669
770
  return /* @__PURE__ */ jsxs("div", { className, children: [
@@ -709,12 +810,6 @@ var PlayruoEmbed = ({
709
810
  },
710
811
  onLoad: () => {
711
812
  setIsIframeLoaded(true);
712
- setTimeout(() => {
713
- if (iframeRef.current) {
714
- iframeRef.current.focus();
715
- iframeRef.current.contentWindow?.focus();
716
- }
717
- }, 500);
718
813
  },
719
814
  onError: () => {
720
815
  console.error("[PlayruoEmbed] Iframe failed to load");
@@ -890,20 +985,11 @@ var EmbedTester = (props = {}) => {
890
985
  addEvent(message);
891
986
  }, [addEvent]);
892
987
  const handleApply = useCallback2(() => {
893
- if (!hasPendingConfigChanges) {
894
- return;
895
- }
896
988
  setAppliedOperation(operationDraft);
897
989
  setAppliedStoreUrl(storeUrlDraft);
898
990
  setAppliedEnableWindowedLaunch(enableWindowedLaunchDraft);
899
991
  resetEmbedSession("Applied updated embed settings");
900
- }, [
901
- enableWindowedLaunchDraft,
902
- hasPendingConfigChanges,
903
- operationDraft,
904
- resetEmbedSession,
905
- storeUrlDraft
906
- ]);
992
+ }, [enableWindowedLaunchDraft, operationDraft, resetEmbedSession, storeUrlDraft]);
907
993
  const handleRestartSession = useCallback2(() => {
908
994
  resetEmbedSession("Embed restarted with applied settings");
909
995
  }, [resetEmbedSession]);
@@ -2007,17 +2093,19 @@ var isPlayruoEmbedMessage = (data) => {
2007
2093
  if (!data || typeof data !== "object") return false;
2008
2094
  const msg = data;
2009
2095
  if (typeof msg.type !== "string") return false;
2010
- return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error" || msg.type === "playruo_session_over" || msg.type === "playruo_stream_start" || msg.type === "playruo_stream_stop" || msg.type === "REQUEST_EXIT_FULLSCREEN" || msg.type === "playruo_event";
2096
+ return msg.type === "playruo_ready" || msg.type === "playruo_state_update" || msg.type === "playruo_purchase_request" || msg.type === "playruo_demo_completed" || msg.type === "playruo_error" || msg.type === "playruo_session_over" || msg.type === "playruo_stream_start" || msg.type === "playruo_stream_stop" || msg.type === "REQUEST_EXIT_FULLSCREEN" || msg.type === "playruo_event" || msg.type === "playruo_password_required" || msg.type === "playruo_password_not_required" || msg.type === "playruo_password_result";
2011
2097
  };
2012
2098
  var isPlayruoInternalMessage = (data) => {
2013
2099
  if (!data || typeof data !== "object") return false;
2014
2100
  const msg = data;
2015
- return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute") && msg.fromEmbed === true;
2101
+ return msg.type === "playruo" && typeof msg.action === "string" && (msg.action === "tryNowClick" || msg.action === "exitFullscreen" || msg.action === "mute" || msg.action === "unmute" || msg.action === "submitPassword" || msg.action === "requestPassword") && msg.fromEmbed === true;
2016
2102
  };
2017
2103
  export {
2018
2104
  EmbedTester,
2105
+ PLAYRUO_UUID_STORAGE_KEY,
2019
2106
  PlayruoEmbed,
2020
2107
  buildEmbedUrl,
2021
2108
  isPlayruoEmbedMessage,
2022
- isPlayruoInternalMessage
2109
+ isPlayruoInternalMessage,
2110
+ resolveVisitorUuid
2023
2111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playruo/embed",
3
- "version": "1.3.3",
3
+ "version": "1.5.0",
4
4
  "description": "React SDK to embed Playruo with postMessage API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,27 +9,52 @@
9
9
  "types": "dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js",
14
- "require": "./dist/index.cjs",
15
- "default": "./dist/index.js"
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
16
20
  }
17
21
  },
18
- "files": ["dist"],
22
+ "files": [
23
+ "dist"
24
+ ],
19
25
  "sideEffects": false,
20
26
  "peerDependencies": {
21
27
  "react": ">=18",
22
28
  "react-dom": ">=18"
23
29
  },
24
30
  "devDependencies": {
31
+ "@arethetypeswrong/cli": "^0.18.5",
32
+ "@testing-library/jest-dom": "^6.6.3",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@testing-library/user-event": "^14.6.1",
35
+ "@types/node": "^26.2.0",
36
+ "@types/react": "^19",
37
+ "@types/react-dom": "^19",
38
+ "@vitest/browser": "^3.2.7",
39
+ "@vitest/coverage-v8": "^3.2.4",
40
+ "jsdom": "^26.1.0",
41
+ "playwright": "^1.62.1",
42
+ "react": "^19.1.0",
43
+ "react-dom": "^19.1.0",
25
44
  "tsup": "^8.0.0",
26
45
  "typescript": "^5.4.0",
27
- "@types/react": "^19",
28
- "@types/react-dom": "^19"
46
+ "vitest": "^3.2.4"
29
47
  },
30
48
  "scripts": {
31
49
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
32
50
  "dev": "tsup src/index.ts --format esm --dts --watch",
33
- "prepare": "npm run build"
51
+ "prepare": "npm run build",
52
+ "typecheck": "tsc -p tsconfig.test.json",
53
+ "check:exports": "attw --pack .",
54
+ "test": "npm run build && vitest run --coverage",
55
+ "test:browser": "vitest run --config vitest.browser.config.ts",
56
+ "test:watch": "npm run build && vitest",
57
+ "docs:notion:dry-run": "node scripts/sync-notion-docs.mjs",
58
+ "docs:notion:publish": "node scripts/sync-notion-docs.mjs --publish"
34
59
  }
35
60
  }