@ait-co/polyfill 0.1.1 → 0.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","names":["#status"],"sources":["../../src/detect.ts","../../src/shims/network.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as\n * an **optional** peer dependency. If it resolves and exposes a known export,\n * we assume we can route calls through it; otherwise we fall back to the\n * browser's native implementation in each shim.\n *\n * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK\n * function during detection (could prompt permission dialogs, fire analytics,\n * etc.).\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n // Presence of a well-known export is our smoke test.\n cached = typeof mod?.getClipboardText === 'function';\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.onLine` + `navigator.connection` shim.\n *\n * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and\n * refreshed on read (throttled):\n * - `'OFFLINE'` → `onLine = false`\n * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)\n * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`\n * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)\n *\n * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`\n * read through to the native value. Install installs own-instance getters\n * that consult the Toss-seeded cache first; when the cache is empty (which\n * it always is in browser mode), the getter temporarily removes its own\n * shadow, reads the prototype value, and reinstates the shadow.\n *\n * Uninstall `delete`s the instance-level override so the prototype descriptor\n * (where `onLine` and `connection` actually live in real browsers) becomes\n * visible again. We never mutate the prototype — doing so would throw in\n * browsers where the descriptor is non-configurable.\n *\n * Caveat: the Web NetworkInformation API is evented (`change` fires on\n * transitions). The SDK exposes only a one-shot query, so listeners attached\n * to `navigator.connection` are accepted but never fire from a `change` event\n * unless the shim observes a real status transition. Synthesising richer\n * events via polling is tracked in TODO.md.\n *\n * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in\n * the install closure. On uninstall the instance-level override is removed,\n * but listeners the consumer attached to the old instance stay bound to that\n * (now-orphan) object and will not see events from a subsequent install.\n * Consumers should re-attach listeners after each install.\n *\n * Seed-boundary race: in Toss mode, reads before the install-time SDK seed\n * completes fall through to the native `navigator.connection`. After the seed\n * lands, subsequent reads return the shim's ShimConnection. Consumers that\n * specifically need the ShimConnection instance (e.g., to attach `change`\n * listeners that fire on Toss network transitions) should wait a microtask\n * after `install()` before attaching listeners, or accept that pre-seed\n * reads may return the native object.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\n\nconst INSTALLED_KEY = Symbol.for('@ait-co/polyfill/network.installed');\n\ninterface BackupHost {\n [INSTALLED_KEY]?: boolean;\n}\n\ntype SdkNetworkStatus = 'OFFLINE' | 'WIFI' | '2G' | '3G' | '4G' | '5G' | 'WWAN' | 'UNKNOWN';\ntype EffectiveType = 'slow-2g' | '2g' | '3g' | '4g';\n\nconst REFRESH_THROTTLE_MS = 500;\n\nfunction statusToOnline(status: SdkNetworkStatus): boolean {\n return status !== 'OFFLINE';\n}\n\nfunction statusToEffectiveType(status: SdkNetworkStatus): EffectiveType {\n switch (status) {\n case '2G':\n return '2g';\n case '3G':\n return '3g';\n default:\n return '4g';\n }\n}\n\nfunction statusToConnectionType(status: SdkNetworkStatus): string {\n switch (status) {\n case 'WIFI':\n return 'wifi';\n case '2G':\n case '3G':\n case '4G':\n case '5G':\n case 'WWAN':\n return 'cellular';\n case 'OFFLINE':\n return 'none';\n default:\n return 'unknown';\n }\n}\n\n// Symbol-keyed setter: the install closure can mutate status without exposing\n// a `setStatus` name on `navigator.connection` (real NetworkInformation has\n// no mutator). `Object.getOwnPropertySymbols(navigator.connection)` returns\n// nothing, so casual enumeration can't find it. A determined caller walking\n// the prototype chain (`Object.getOwnPropertySymbols(Object.getPrototypeOf(...))`)\n// can still surface the symbol — there is no trust boundary between polyfill\n// and consumer code in the same realm, so this is a discouragement, not a\n// security control.\nconst SET_STATUS = Symbol('@ait-co/polyfill/network.setStatus');\n\nclass ShimConnection extends EventTarget {\n #status: SdkNetworkStatus | null = null;\n onchange: ((this: ShimConnection, ev: Event) => unknown) | null = null;\n\n constructor() {\n super();\n // Forward `change` events to the legacy `onchange` handler for parity with\n // the NetworkInformation API.\n this.addEventListener('change', (ev) => this.onchange?.call(this, ev));\n }\n\n [SET_STATUS](next: SdkNetworkStatus | null): void {\n this.#status = next;\n }\n\n get effectiveType(): EffectiveType {\n return statusToEffectiveType(this.#status ?? 'UNKNOWN');\n }\n // `downlink` / `rtt` / `saveData` are placeholders — the SDK does not expose\n // these. We return 0/false rather than fabricate plausible numbers. Noted\n // in CLAUDE.md.\n get downlink(): number {\n return 0;\n }\n get rtt(): number {\n return 0;\n }\n get saveData(): boolean {\n return false;\n }\n get type(): string {\n return statusToConnectionType(this.#status ?? 'UNKNOWN');\n }\n}\n\nexport function installNetworkShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (host[INSTALLED_KEY]) {\n return () => uninstallNetworkShim();\n }\n host[INSTALLED_KEY] = true;\n\n // Per-install state. Kept in closure so uninstall/reinstall cycles don't\n // leak state between instances (module-scope would leak across tests).\n let cachedStatus: SdkNetworkStatus | null = null;\n let lastRefresh = 0;\n let inflight: Promise<void> | null = null;\n const connection = new ShimConnection();\n\n async function refresh(): Promise<void> {\n // Coalesce concurrent refreshes — without this, rapid reads during an\n // in-flight SDK call each set `lastRefresh` and return early, without\n // anyone actually fetching fresh data.\n if (inflight) return inflight;\n const now = Date.now();\n if (now - lastRefresh < REFRESH_THROTTLE_MS) return;\n inflight = (async () => {\n try {\n if (!(await isTossEnvironment())) return;\n const sdk = await loadTossSdk();\n const fn = (sdk as { getNetworkStatus?: unknown } | null)?.getNetworkStatus;\n if (typeof fn !== 'function') return;\n const next = (await (fn as () => Promise<SdkNetworkStatus>)()) as SdkNetworkStatus;\n const prev = cachedStatus;\n cachedStatus = next;\n connection[SET_STATUS](next);\n // Only dispatch `change` on real transitions — the null → X seed on\n // first install is learning, not a transition, and would otherwise\n // mis-trigger consumer handlers.\n if (prev !== null && prev !== next) {\n connection.dispatchEvent(new Event('change'));\n }\n } catch {\n // Advisory — refresh failures keep the prior cache. `void refresh()`\n // callers would otherwise surface unhandled rejections if\n // isTossEnvironment / loadTossSdk / getNetworkStatus ever throw.\n } finally {\n lastRefresh = Date.now();\n inflight = null;\n }\n })();\n return inflight;\n }\n\n // Seed the cache on install so the first sync read is meaningful.\n void refresh();\n\n Object.defineProperty(navigator, 'onLine', {\n configurable: true,\n get() {\n void refresh();\n if (cachedStatus !== null) {\n return statusToOnline(cachedStatus);\n }\n // Fall back to whatever the prototype would have returned. Temporarily\n // delete our shadow to read through; the try/finally guarantees the\n // shadow is restored even if the prototype getter throws.\n const desc = Object.getOwnPropertyDescriptor(navigator, 'onLine');\n delete (navigator as unknown as { onLine?: boolean }).onLine;\n try {\n return navigator.onLine;\n } finally {\n if (desc) Object.defineProperty(navigator, 'onLine', desc);\n }\n },\n });\n\n Object.defineProperty(navigator, 'connection', {\n configurable: true,\n get() {\n void refresh();\n // Symmetric with `onLine`: when the SDK hasn't seeded us (either a\n // browser-mode install or pre-seed Toss), read through to the native\n // `navigator.connection` so consumers in plain browsers don't see a\n // hardcoded `effectiveType: '4g'` default.\n if (cachedStatus === null) {\n const desc = Object.getOwnPropertyDescriptor(navigator, 'connection');\n delete (navigator as unknown as { connection?: unknown }).connection;\n try {\n const native = (navigator as Navigator & { connection?: unknown }).connection;\n if (native !== undefined) return native;\n } finally {\n if (desc) Object.defineProperty(navigator, 'connection', desc);\n }\n }\n return connection;\n },\n });\n\n return uninstallNetworkShim;\n}\n\nexport function uninstallNetworkShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!host[INSTALLED_KEY]) return;\n\n // `delete` the instance-level property so the prototype descriptor (where\n // `onLine` and `connection` actually live in real browsers) is exposed\n // again. Redefining the prototype would throw on non-configurable getters.\n delete (navigator as unknown as { onLine?: boolean }).onLine;\n delete (navigator as unknown as { connection?: unknown }).connection;\n\n delete host[INSTALLED_KEY];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"network.js","names":["#status"],"sources":["../../src/detect.ts","../../src/shims/network.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.onLine` + `navigator.connection` shim.\n *\n * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and\n * refreshed on read (throttled):\n * - `'OFFLINE'` → `onLine = false`\n * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)\n * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`\n * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)\n *\n * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`\n * read through to the native value. Install installs own-instance getters\n * that consult the Toss-seeded cache first; when the cache is empty (which\n * it always is in browser mode), the getter temporarily removes its own\n * shadow, reads the prototype value, and reinstates the shadow.\n *\n * Uninstall `delete`s the instance-level override so the prototype descriptor\n * (where `onLine` and `connection` actually live in real browsers) becomes\n * visible again. We never mutate the prototype — doing so would throw in\n * browsers where the descriptor is non-configurable.\n *\n * Caveat: the Web NetworkInformation API is evented (`change` fires on\n * transitions). The SDK exposes only a one-shot query, so listeners attached\n * to `navigator.connection` are accepted but never fire from a `change` event\n * unless the shim observes a real status transition. Synthesising richer\n * events via polling is tracked in TODO.md.\n *\n * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in\n * the install closure. On uninstall the instance-level override is removed,\n * but listeners the consumer attached to the old instance stay bound to that\n * (now-orphan) object and will not see events from a subsequent install.\n * Consumers should re-attach listeners after each install.\n *\n * Seed-boundary race: in Toss mode, reads before the install-time SDK seed\n * completes fall through to the native `navigator.connection`. After the seed\n * lands, subsequent reads return the shim's ShimConnection. Consumers that\n * specifically need the ShimConnection instance (e.g., to attach `change`\n * listeners that fire on Toss network transitions) should wait a microtask\n * after `install()` before attaching listeners, or accept that pre-seed\n * reads may return the native object.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\n\nconst INSTALLED_KEY = Symbol.for('@ait-co/polyfill/network.installed');\n\ninterface BackupHost {\n [INSTALLED_KEY]?: boolean;\n}\n\ntype SdkNetworkStatus = 'OFFLINE' | 'WIFI' | '2G' | '3G' | '4G' | '5G' | 'WWAN' | 'UNKNOWN';\ntype EffectiveType = 'slow-2g' | '2g' | '3g' | '4g';\n\nconst REFRESH_THROTTLE_MS = 500;\n\nfunction statusToOnline(status: SdkNetworkStatus): boolean {\n return status !== 'OFFLINE';\n}\n\nfunction statusToEffectiveType(status: SdkNetworkStatus): EffectiveType {\n switch (status) {\n case '2G':\n return '2g';\n case '3G':\n return '3g';\n default:\n return '4g';\n }\n}\n\nfunction statusToConnectionType(status: SdkNetworkStatus): string {\n switch (status) {\n case 'WIFI':\n return 'wifi';\n case '2G':\n case '3G':\n case '4G':\n case '5G':\n case 'WWAN':\n return 'cellular';\n case 'OFFLINE':\n return 'none';\n default:\n return 'unknown';\n }\n}\n\n// Symbol-keyed setter: the install closure can mutate status without exposing\n// a `setStatus` name on `navigator.connection` (real NetworkInformation has\n// no mutator). `Object.getOwnPropertySymbols(navigator.connection)` returns\n// nothing, so casual enumeration can't find it. A determined caller walking\n// the prototype chain (`Object.getOwnPropertySymbols(Object.getPrototypeOf(...))`)\n// can still surface the symbol — there is no trust boundary between polyfill\n// and consumer code in the same realm, so this is a discouragement, not a\n// security control.\nconst SET_STATUS = Symbol('@ait-co/polyfill/network.setStatus');\n\nclass ShimConnection extends EventTarget {\n #status: SdkNetworkStatus | null = null;\n onchange: ((this: ShimConnection, ev: Event) => unknown) | null = null;\n\n constructor() {\n super();\n // Forward `change` events to the legacy `onchange` handler for parity with\n // the NetworkInformation API.\n this.addEventListener('change', (ev) => this.onchange?.call(this, ev));\n }\n\n [SET_STATUS](next: SdkNetworkStatus | null): void {\n this.#status = next;\n }\n\n get effectiveType(): EffectiveType {\n return statusToEffectiveType(this.#status ?? 'UNKNOWN');\n }\n // `downlink` / `rtt` / `saveData` are placeholders — the SDK does not expose\n // these. We return 0/false rather than fabricate plausible numbers. Noted\n // in CLAUDE.md.\n get downlink(): number {\n return 0;\n }\n get rtt(): number {\n return 0;\n }\n get saveData(): boolean {\n return false;\n }\n get type(): string {\n return statusToConnectionType(this.#status ?? 'UNKNOWN');\n }\n}\n\nexport function installNetworkShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (host[INSTALLED_KEY]) {\n return () => uninstallNetworkShim();\n }\n host[INSTALLED_KEY] = true;\n\n // Per-install state. Kept in closure so uninstall/reinstall cycles don't\n // leak state between instances (module-scope would leak across tests).\n let cachedStatus: SdkNetworkStatus | null = null;\n let lastRefresh = 0;\n let inflight: Promise<void> | null = null;\n const connection = new ShimConnection();\n\n async function refresh(): Promise<void> {\n // Coalesce concurrent refreshes — without this, rapid reads during an\n // in-flight SDK call each set `lastRefresh` and return early, without\n // anyone actually fetching fresh data.\n if (inflight) return inflight;\n const now = Date.now();\n if (now - lastRefresh < REFRESH_THROTTLE_MS) return;\n inflight = (async () => {\n try {\n if (!(await isTossEnvironment())) return;\n const sdk = await loadTossSdk();\n const fn = (sdk as { getNetworkStatus?: unknown } | null)?.getNetworkStatus;\n if (typeof fn !== 'function') return;\n const next = (await (fn as () => Promise<SdkNetworkStatus>)()) as SdkNetworkStatus;\n const prev = cachedStatus;\n cachedStatus = next;\n connection[SET_STATUS](next);\n // Only dispatch `change` on real transitions — the null → X seed on\n // first install is learning, not a transition, and would otherwise\n // mis-trigger consumer handlers.\n if (prev !== null && prev !== next) {\n connection.dispatchEvent(new Event('change'));\n }\n } catch {\n // Advisory — refresh failures keep the prior cache. `void refresh()`\n // callers would otherwise surface unhandled rejections if\n // isTossEnvironment / loadTossSdk / getNetworkStatus ever throw.\n } finally {\n lastRefresh = Date.now();\n inflight = null;\n }\n })();\n return inflight;\n }\n\n // Seed the cache on install so the first sync read is meaningful.\n void refresh();\n\n Object.defineProperty(navigator, 'onLine', {\n configurable: true,\n get() {\n void refresh();\n if (cachedStatus !== null) {\n return statusToOnline(cachedStatus);\n }\n // Fall back to whatever the prototype would have returned. Temporarily\n // delete our shadow to read through; the try/finally guarantees the\n // shadow is restored even if the prototype getter throws.\n const desc = Object.getOwnPropertyDescriptor(navigator, 'onLine');\n delete (navigator as unknown as { onLine?: boolean }).onLine;\n try {\n return navigator.onLine;\n } finally {\n if (desc) Object.defineProperty(navigator, 'onLine', desc);\n }\n },\n });\n\n Object.defineProperty(navigator, 'connection', {\n configurable: true,\n get() {\n void refresh();\n // Symmetric with `onLine`: when the SDK hasn't seeded us (either a\n // browser-mode install or pre-seed Toss), read through to the native\n // `navigator.connection` so consumers in plain browsers don't see a\n // hardcoded `effectiveType: '4g'` default.\n if (cachedStatus === null) {\n const desc = Object.getOwnPropertyDescriptor(navigator, 'connection');\n delete (navigator as unknown as { connection?: unknown }).connection;\n try {\n const native = (navigator as Navigator & { connection?: unknown }).connection;\n if (native !== undefined) return native;\n } finally {\n if (desc) Object.defineProperty(navigator, 'connection', desc);\n }\n }\n return connection;\n },\n });\n\n return uninstallNetworkShim;\n}\n\nexport function uninstallNetworkShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!host[INSTALLED_KEY]) return;\n\n // `delete` the instance-level property so the prototype descriptor (where\n // `onLine` and `connection` actually live in real browsers) is exposed\n // again. Redefining the prototype would throw on non-configurable getters.\n delete (navigator as unknown as { onLine?: boolean }).onLine;\n delete (navigator as unknown as { connection?: unknown }).connection;\n\n delete host[INSTALLED_KEY];\n}\n"],"mappings":"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"}
@@ -2,14 +2,19 @@
2
2
  /**
3
3
  * Environment detection: are we running inside Apps in Toss, or a plain browser?
4
4
  *
5
- * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as
6
- * an **optional** peer dependency. If it resolves and exposes a known export,
7
- * we assume we can route calls through it; otherwise we fall back to the
8
- * browser's native implementation in each shim.
5
+ * Strategy: call the SDK's `getAppsInTossGlobals()` a synchronous export
6
+ * that returns the runtime's Toss globals (deploymentId, brand name, …)
7
+ * inside the Apps in Toss runtime and throws (RN bridge unavailable)
8
+ * anywhere else. The SDK itself is an **optional** peer dependency; if its
9
+ * module can't be imported we are definitely not inside Toss.
9
10
  *
10
- * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK
11
- * function during detection (could prompt permission dialogs, fire analytics,
12
- * etc.).
11
+ * Just having the SDK module resolvable is not enough apps can bundle it
12
+ * and still run in a plain browser. We need the bridge probe to confirm.
13
+ *
14
+ * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but
15
+ * that's a constant read from the bridge — no permission dialogs, no
16
+ * analytics fire. In a plain browser the bridge lookup fails fast (sync
17
+ * throw, microsecond-scale), so the startup cost is negligible.
13
18
  */
14
19
  let cached;
15
20
  /**
@@ -39,7 +44,17 @@ async function isTossEnvironment() {
39
44
  if (force === "toss") return true;
40
45
  if (force === "browser") return false;
41
46
  if (cached !== void 0) return cached;
42
- cached = typeof (await loadTossSdk())?.getClipboardText === "function";
47
+ const mod = await loadTossSdk();
48
+ if (typeof mod?.getAppsInTossGlobals !== "function") {
49
+ cached = false;
50
+ return cached;
51
+ }
52
+ try {
53
+ const globals = mod.getAppsInTossGlobals();
54
+ cached = Boolean(globals) && typeof globals === "object";
55
+ } catch {
56
+ cached = false;
57
+ }
43
58
  return cached;
44
59
  }
45
60
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"share.js","names":[],"sources":["../../src/detect.ts","../../src/shims/share.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as\n * an **optional** peer dependency. If it resolves and exposes a known export,\n * we assume we can route calls through it; otherwise we fall back to the\n * browser's native implementation in each shim.\n *\n * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK\n * function during detection (could prompt permission dialogs, fire analytics,\n * etc.).\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n // Presence of a well-known export is our smoke test.\n cached = typeof mod?.getClipboardText === 'function';\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.share` shim.\n *\n * Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only\n * accepts a single `message` string, so we concatenate `title`, `text`, and\n * `url` with newline separators (skipping missing/empty values).\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.share`, or\n * throws `NotSupportedError` if unavailable.\n *\n * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).\n * `canShare({ files })` returns `false` whenever the sync-accessible detection\n * says Toss is active (or is being forced via the test override).\n */\n\nimport { isTossEnvironment, isTossEnvironmentCached, loadTossSdk } from '../detect.js';\n\nconst SHARE_BACKUP_KEY = Symbol.for('@ait-co/polyfill/share.original');\n\ntype ShareFn = (data?: ShareData) => Promise<void>;\ntype CanShareFn = (data?: ShareData) => boolean;\n\ninterface Backup {\n share?: ShareFn | undefined;\n canShare?: CanShareFn | undefined;\n hadShare: boolean;\n hadCanShare: boolean;\n}\n\ninterface BackupHost {\n [SHARE_BACKUP_KEY]?: Backup | undefined;\n}\n\nfunction buildSdkMessage(data: ShareData | undefined): string {\n // Use presence checks rather than truthiness so an intentionally empty\n // string in one field is handled correctly alongside a non-empty sibling.\n const parts: string[] = [];\n if (data?.title != null && data.title !== '') parts.push(data.title);\n if (data?.text != null && data.text !== '') parts.push(data.text);\n if (data?.url != null && data.url !== '') parts.push(data.url);\n return parts.join('\\n');\n}\n\nasync function shareShim(data?: ShareData): Promise<void> {\n if (await isTossEnvironment()) {\n const sdk = await loadTossSdk();\n const fn = (sdk as { share?: unknown } | null)?.share;\n if (typeof fn === 'function') {\n const message = buildSdkMessage(data);\n if (!message) {\n throw new TypeError(\n '[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.',\n );\n }\n try {\n await (fn as (o: { message: string }) => Promise<void>)({ message });\n } catch (e) {\n // Spec says navigator.share rejects with a DOMException. Wrap SDK\n // errors as AbortError (the most common cause is user cancellation),\n // attaching the original as `.cause` for Sentry-style telemetry.\n const message_ = e instanceof Error ? e.message : String(e);\n const wrapped = new DOMException(message_, 'AbortError');\n if (e instanceof Error) {\n (wrapped as Error).cause = e;\n }\n throw wrapped;\n }\n return;\n }\n }\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const original = backup?.share;\n if (!original) {\n throw new DOMException(\n '[@ait-co/polyfill] navigator.share is not available in this environment.',\n 'NotSupportedError',\n );\n }\n return original.call(navigator, data);\n}\n\nfunction canShareShim(data?: ShareData): boolean {\n const hasFiles = Boolean(data?.files && data.files.length > 0);\n const toss = isTossEnvironmentCached();\n\n if (hasFiles) {\n // SDK does not share files. If we know we're in Toss (or it's being\n // forced), say so honestly. If detection hasn't resolved yet, be\n // pessimistic — a false negative is safer than promising a capability\n // we'll turn around and deny.\n if (toss === true) return false;\n if (toss === undefined) return false;\n }\n\n // Toss with non-file payloads: true iff there's at least one field.\n if (toss === true) {\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n }\n\n // `toss === undefined` (detection not resolved) with non-file payload falls\n // through to the browser-native answer. Rationale: `canShare` is rarely\n // load-bearing — consumers care about `share()` itself, which awaits the\n // async detection correctly. A false-negative here would needlessly hide a\n // Share button while detection settles.\n // Browser path: delegate to native when present.\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const originalCanShare = backup?.canShare;\n if (originalCanShare) {\n return originalCanShare.call(navigator, data);\n }\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n}\n\nexport function installShareShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (SHARE_BACKUP_KEY in host) {\n // Already installed. Use `in` so the absence of `share` / `canShare` on\n // the pre-install navigator (legitimately stored as `undefined`) doesn't\n // re-trigger install.\n return () => uninstallShareShim();\n }\n\n const nav = navigator as Navigator & {\n share?: ShareFn;\n canShare?: CanShareFn;\n };\n host[SHARE_BACKUP_KEY] = {\n share: nav.share,\n canShare: nav.canShare,\n hadShare: 'share' in nav,\n hadCanShare: 'canShare' in nav,\n };\n\n Object.defineProperty(navigator, 'share', {\n value: shareShim,\n configurable: true,\n writable: true,\n });\n Object.defineProperty(navigator, 'canShare', {\n value: canShareShim,\n configurable: true,\n writable: true,\n });\n\n return uninstallShareShim;\n}\n\nexport function uninstallShareShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(SHARE_BACKUP_KEY in host)) return;\n\n const backup = host[SHARE_BACKUP_KEY];\n\n // Prototype-safe restore: delete the instance override first so a prototype\n // descriptor (real browsers put `share` / `canShare` on `Navigator.prototype`\n // when they exist at all) shows through. Only redefine on the instance if\n // the original was an own property that the prototype doesn't provide —\n // otherwise we'd permanently shadow the prototype getter.\n delete (navigator as unknown as { share?: ShareFn }).share;\n if (backup?.hadShare && navigator.share !== backup.share) {\n Object.defineProperty(navigator, 'share', {\n value: backup.share,\n configurable: true,\n writable: true,\n });\n }\n delete (navigator as unknown as { canShare?: CanShareFn }).canShare;\n if (backup?.hadCanShare && navigator.canShare !== backup.canShare) {\n Object.defineProperty(navigator, 'canShare', {\n value: backup.canShare,\n configurable: true,\n writable: true,\n });\n }\n\n delete host[SHARE_BACKUP_KEY];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"share.js","names":[],"sources":["../../src/detect.ts","../../src/shims/share.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.share` shim.\n *\n * Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only\n * accepts a single `message` string, so we concatenate `title`, `text`, and\n * `url` with newline separators (skipping missing/empty values).\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.share`, or\n * throws `NotSupportedError` if unavailable.\n *\n * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).\n * `canShare({ files })` returns `false` whenever the sync-accessible detection\n * says Toss is active (or is being forced via the test override).\n */\n\nimport { isTossEnvironment, isTossEnvironmentCached, loadTossSdk } from '../detect.js';\n\nconst SHARE_BACKUP_KEY = Symbol.for('@ait-co/polyfill/share.original');\n\ntype ShareFn = (data?: ShareData) => Promise<void>;\ntype CanShareFn = (data?: ShareData) => boolean;\n\ninterface Backup {\n share?: ShareFn | undefined;\n canShare?: CanShareFn | undefined;\n hadShare: boolean;\n hadCanShare: boolean;\n}\n\ninterface BackupHost {\n [SHARE_BACKUP_KEY]?: Backup | undefined;\n}\n\nfunction buildSdkMessage(data: ShareData | undefined): string {\n // Use presence checks rather than truthiness so an intentionally empty\n // string in one field is handled correctly alongside a non-empty sibling.\n const parts: string[] = [];\n if (data?.title != null && data.title !== '') parts.push(data.title);\n if (data?.text != null && data.text !== '') parts.push(data.text);\n if (data?.url != null && data.url !== '') parts.push(data.url);\n return parts.join('\\n');\n}\n\nasync function shareShim(data?: ShareData): Promise<void> {\n if (await isTossEnvironment()) {\n const sdk = await loadTossSdk();\n const fn = (sdk as { share?: unknown } | null)?.share;\n if (typeof fn === 'function') {\n const message = buildSdkMessage(data);\n if (!message) {\n throw new TypeError(\n '[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.',\n );\n }\n try {\n await (fn as (o: { message: string }) => Promise<void>)({ message });\n } catch (e) {\n // Spec says navigator.share rejects with a DOMException. Wrap SDK\n // errors as AbortError (the most common cause is user cancellation),\n // attaching the original as `.cause` for Sentry-style telemetry.\n const message_ = e instanceof Error ? e.message : String(e);\n const wrapped = new DOMException(message_, 'AbortError');\n if (e instanceof Error) {\n (wrapped as Error).cause = e;\n }\n throw wrapped;\n }\n return;\n }\n }\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const original = backup?.share;\n if (!original) {\n throw new DOMException(\n '[@ait-co/polyfill] navigator.share is not available in this environment.',\n 'NotSupportedError',\n );\n }\n return original.call(navigator, data);\n}\n\nfunction canShareShim(data?: ShareData): boolean {\n const hasFiles = Boolean(data?.files && data.files.length > 0);\n const toss = isTossEnvironmentCached();\n\n if (hasFiles) {\n // SDK does not share files. If we know we're in Toss (or it's being\n // forced), say so honestly. If detection hasn't resolved yet, be\n // pessimistic — a false negative is safer than promising a capability\n // we'll turn around and deny.\n if (toss === true) return false;\n if (toss === undefined) return false;\n }\n\n // Toss with non-file payloads: true iff there's at least one field.\n if (toss === true) {\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n }\n\n // `toss === undefined` (detection not resolved) with non-file payload falls\n // through to the browser-native answer. Rationale: `canShare` is rarely\n // load-bearing — consumers care about `share()` itself, which awaits the\n // async detection correctly. A false-negative here would needlessly hide a\n // Share button while detection settles.\n // Browser path: delegate to native when present.\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const originalCanShare = backup?.canShare;\n if (originalCanShare) {\n return originalCanShare.call(navigator, data);\n }\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n}\n\nexport function installShareShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (SHARE_BACKUP_KEY in host) {\n // Already installed. Use `in` so the absence of `share` / `canShare` on\n // the pre-install navigator (legitimately stored as `undefined`) doesn't\n // re-trigger install.\n return () => uninstallShareShim();\n }\n\n const nav = navigator as Navigator & {\n share?: ShareFn;\n canShare?: CanShareFn;\n };\n host[SHARE_BACKUP_KEY] = {\n share: nav.share,\n canShare: nav.canShare,\n hadShare: 'share' in nav,\n hadCanShare: 'canShare' in nav,\n };\n\n Object.defineProperty(navigator, 'share', {\n value: shareShim,\n configurable: true,\n writable: true,\n });\n Object.defineProperty(navigator, 'canShare', {\n value: canShareShim,\n configurable: true,\n writable: true,\n });\n\n return uninstallShareShim;\n}\n\nexport function uninstallShareShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(SHARE_BACKUP_KEY in host)) return;\n\n const backup = host[SHARE_BACKUP_KEY];\n\n // Prototype-safe restore: delete the instance override first so a prototype\n // descriptor (real browsers put `share` / `canShare` on `Navigator.prototype`\n // when they exist at all) shows through. Only redefine on the instance if\n // the original was an own property that the prototype doesn't provide —\n // otherwise we'd permanently shadow the prototype getter.\n delete (navigator as unknown as { share?: ShareFn }).share;\n if (backup?.hadShare && navigator.share !== backup.share) {\n Object.defineProperty(navigator, 'share', {\n value: backup.share,\n configurable: true,\n writable: true,\n });\n }\n delete (navigator as unknown as { canShare?: CanShareFn }).canShare;\n if (backup?.hadCanShare && navigator.canShare !== backup.canShare) {\n Object.defineProperty(navigator, 'canShare', {\n value: backup.canShare,\n configurable: true,\n writable: true,\n });\n }\n\n delete host[SHARE_BACKUP_KEY];\n}\n"],"mappings":"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"}
@@ -2,14 +2,19 @@
2
2
  /**
3
3
  * Environment detection: are we running inside Apps in Toss, or a plain browser?
4
4
  *
5
- * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as
6
- * an **optional** peer dependency. If it resolves and exposes a known export,
7
- * we assume we can route calls through it; otherwise we fall back to the
8
- * browser's native implementation in each shim.
5
+ * Strategy: call the SDK's `getAppsInTossGlobals()` a synchronous export
6
+ * that returns the runtime's Toss globals (deploymentId, brand name, …)
7
+ * inside the Apps in Toss runtime and throws (RN bridge unavailable)
8
+ * anywhere else. The SDK itself is an **optional** peer dependency; if its
9
+ * module can't be imported we are definitely not inside Toss.
9
10
  *
10
- * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK
11
- * function during detection (could prompt permission dialogs, fire analytics,
12
- * etc.).
11
+ * Just having the SDK module resolvable is not enough apps can bundle it
12
+ * and still run in a plain browser. We need the bridge probe to confirm.
13
+ *
14
+ * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but
15
+ * that's a constant read from the bridge — no permission dialogs, no
16
+ * analytics fire. In a plain browser the bridge lookup fails fast (sync
17
+ * throw, microsecond-scale), so the startup cost is negligible.
13
18
  */
14
19
  let cached;
15
20
  /**
@@ -24,7 +29,17 @@ async function isTossEnvironment() {
24
29
  if (force === "toss") return true;
25
30
  if (force === "browser") return false;
26
31
  if (cached !== void 0) return cached;
27
- cached = typeof (await loadTossSdk())?.getClipboardText === "function";
32
+ const mod = await loadTossSdk();
33
+ if (typeof mod?.getAppsInTossGlobals !== "function") {
34
+ cached = false;
35
+ return cached;
36
+ }
37
+ try {
38
+ const globals = mod.getAppsInTossGlobals();
39
+ cached = Boolean(globals) && typeof globals === "object";
40
+ } catch {
41
+ cached = false;
42
+ }
28
43
  return cached;
29
44
  }
30
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"vibrate.js","names":[],"sources":["../../src/detect.ts","../../src/shims/vibrate.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as\n * an **optional** peer dependency. If it resolves and exposes a known export,\n * we assume we can route calls through it; otherwise we fall back to the\n * browser's native implementation in each shim.\n *\n * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK\n * function during detection (could prompt permission dialogs, fire analytics,\n * etc.).\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n // Presence of a well-known export is our smoke test.\n cached = typeof mod?.getClipboardText === 'function';\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.vibrate` shim.\n *\n * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`:\n * - `vibrate(0)` → no-op (web standard: cancels pending vibration)\n * - `vibrate(number)`: short (< 40ms) → `tickWeak`, long (≥ 40ms) → `basicMedium`\n * - `vibrate(number[])`: iterate \"on\" segments (even indices) as `tap` pulses\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,\n * or returns `false` when unavailable (matches the spec — browsers that don't\n * support vibration simply return `false`).\n *\n * Caveats (documented in CLAUDE.md as the known lossy trade-off):\n * - SDK haptics are qualitative (\"tickWeak\", \"basicMedium\"), not millisecond\n * durations. The shim approximates intensity from duration but cannot\n * reproduce exact patterns.\n * - Arrays are fired sequentially via `setTimeout`; gaps between pulses are\n * honoured only as \"time until the next tap\", not as silent-vs-vibrating.\n * - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return\n * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\n\nconst BACKUP_KEY = Symbol.for('@ait-co/polyfill/vibrate.original');\nconst HAD_KEY = Symbol.for('@ait-co/polyfill/vibrate.hadOriginal');\n\ninterface BackupHost {\n [BACKUP_KEY]?: ((pattern: VibratePattern) => boolean) | undefined;\n [HAD_KEY]?: boolean;\n}\n\nconst SHORT_VIBRATION_MS = 40;\n\ntype HapticType =\n | 'tickWeak'\n | 'tap'\n | 'tickMedium'\n | 'softMedium'\n | 'basicWeak'\n | 'basicMedium'\n | 'success'\n | 'error'\n | 'wiggle'\n | 'confetti';\n\nasync function haptic(type: HapticType): Promise<void> {\n const sdk = await loadTossSdk();\n const fn = (sdk as { generateHapticFeedback?: unknown } | null)?.generateHapticFeedback;\n if (typeof fn === 'function') {\n try {\n await (fn as (o: { type: HapticType }) => Promise<void>)({ type });\n } catch {\n // Best-effort; spec-level `vibrate` cannot surface errors.\n }\n }\n}\n\nfunction durationToHaptic(duration: number): HapticType {\n return duration < SHORT_VIBRATION_MS ? 'tickWeak' : 'basicMedium';\n}\n\nfunction vibrateShim(pattern: VibratePattern): boolean {\n // Matches the spec: `vibrate(0)` or `vibrate([])` cancels pending vibration.\n // We can't cancel an in-flight SDK haptic (no cancel API), but we still\n // forward the cancel to the browser fallback so native vibration stops.\n const arr = Array.isArray(pattern) ? pattern : [pattern];\n if (arr.length === 0 || arr.every((n) => n === 0)) {\n void (async () => {\n if (!(await isTossEnvironment())) {\n const host = navigator as unknown as BackupHost;\n host[BACKUP_KEY]?.call(navigator, pattern);\n }\n })();\n return true;\n }\n\n void (async () => {\n if (await isTossEnvironment()) {\n if (!Array.isArray(pattern)) {\n await haptic(durationToHaptic(pattern));\n return;\n }\n // Even indices = \"on\" durations, odd indices = pauses. `pattern[i]` is\n // `number | undefined` under `noUncheckedIndexedAccess`; the `undefined`\n // case only arises on out-of-bounds, which our length bound prevents.\n for (let i = 0; i < pattern.length; i += 2) {\n const on = pattern[i];\n if (on === undefined) break;\n if (on > 0) {\n await haptic('tap');\n }\n const pause = pattern[i + 1];\n if (typeof pause === 'number' && pause > 0) {\n await new Promise<void>((r) => setTimeout(r, pause));\n }\n }\n return;\n }\n const host = navigator as unknown as BackupHost;\n const original = host[BACKUP_KEY];\n original?.call(navigator, pattern);\n })();\n\n return true;\n}\n\nexport function installVibrateShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (BACKUP_KEY in host) {\n return () => uninstallVibrateShim();\n }\n\n const nav = navigator as Navigator & { vibrate?: (p: VibratePattern) => boolean };\n host[BACKUP_KEY] = nav.vibrate;\n host[HAD_KEY] = 'vibrate' in nav;\n\n Object.defineProperty(navigator, 'vibrate', {\n value: vibrateShim,\n configurable: true,\n writable: true,\n });\n\n return uninstallVibrateShim;\n}\n\nexport function uninstallVibrateShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(BACKUP_KEY in host)) return;\n\n const original = host[BACKUP_KEY];\n const had = host[HAD_KEY];\n // Prototype-safe restore: delete the instance override first, then only\n // redefine on the instance if the original was an own property the\n // prototype doesn't provide — prevents permanent shadowing of a prototype\n // `vibrate` getter on real browsers.\n delete (navigator as unknown as { vibrate?: (p: VibratePattern) => boolean }).vibrate;\n if (had && navigator.vibrate !== original) {\n Object.defineProperty(navigator, 'vibrate', {\n value: original,\n configurable: true,\n writable: true,\n });\n }\n delete host[BACKUP_KEY];\n delete host[HAD_KEY];\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAI;;;;;;;;AAgCJ,eAAsB,oBAAsC;CAG1D,MAAM,QAAQ,WAAW;AACzB,KAAI,UAAU,OAAQ,QAAO;AAC7B,KAAI,UAAU,UAAW,QAAO;AAEhC,KAAI,WAAW,KAAA,EAAW,QAAO;AAIjC,UAAS,QAFG,MAAM,aAAa,GAEV,qBAAqB;AAC1C,QAAO;;;;;;AAOT,eAAsB,cAA4E;AAChG,KAAI;AACF,SAAO,MAAM,OAAO;SACd;AACN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AC5CX,MAAM,aAAa,OAAO,IAAI,oCAAoC;AAClE,MAAM,UAAU,OAAO,IAAI,uCAAuC;AAOlE,MAAM,qBAAqB;AAc3B,eAAe,OAAO,MAAiC;CAErD,MAAM,MADM,MAAM,aAAa,GACkC;AACjE,KAAI,OAAO,OAAO,WAChB,KAAI;AACF,QAAO,GAAkD,EAAE,MAAM,CAAC;SAC5D;;AAMZ,SAAS,iBAAiB,UAA8B;AACtD,QAAO,WAAW,qBAAqB,aAAa;;AAGtD,SAAS,YAAY,SAAkC;CAIrD,MAAM,MAAM,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ;AACxD,KAAI,IAAI,WAAW,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,EAAE;AACjD,GAAM,YAAY;AAChB,OAAI,CAAE,MAAM,mBAAmB,CAChB,WACR,aAAa,KAAK,WAAW,QAAQ;MAE1C;AACJ,SAAO;;AAGT,EAAM,YAAY;AAChB,MAAI,MAAM,mBAAmB,EAAE;AAC7B,OAAI,CAAC,MAAM,QAAQ,QAAQ,EAAE;AAC3B,UAAM,OAAO,iBAAiB,QAAQ,CAAC;AACvC;;AAKF,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,KAAK,QAAQ;AACnB,QAAI,OAAO,KAAA,EAAW;AACtB,QAAI,KAAK,EACP,OAAM,OAAO,MAAM;IAErB,MAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,OAAO,UAAU,YAAY,QAAQ,EACvC,OAAM,IAAI,SAAe,MAAM,WAAW,GAAG,MAAM,CAAC;;AAGxD;;AAEW,YACS,aACZ,KAAK,WAAW,QAAQ;KAChC;AAEJ,QAAO;;AAGT,SAAgB,qBAAiC;AAC/C,KAAI,OAAO,cAAc,YACvB,cAAa;CAGf,MAAM,OAAO;AACb,KAAI,cAAc,KAChB,cAAa,sBAAsB;CAGrC,MAAM,MAAM;AACZ,MAAK,cAAc,IAAI;AACvB,MAAK,WAAW,aAAa;AAE7B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEF,QAAO;;AAGT,SAAgB,uBAA6B;AAC3C,KAAI,OAAO,cAAc,YAAa;CACtC,MAAM,OAAO;AACb,KAAI,EAAE,cAAc,MAAO;CAE3B,MAAM,WAAW,KAAK;CACtB,MAAM,MAAM,KAAK;AAKjB,QAAQ,UAAsE;AAC9E,KAAI,OAAO,UAAU,YAAY,SAC/B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEJ,QAAO,KAAK;AACZ,QAAO,KAAK"}
1
+ {"version":3,"file":"vibrate.js","names":[],"sources":["../../src/detect.ts","../../src/shims/vibrate.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.vibrate` shim.\n *\n * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`:\n * - `vibrate(0)` → no-op (web standard: cancels pending vibration)\n * - `vibrate(number)`: short (< 40ms) → `tickWeak`, long (≥ 40ms) → `basicMedium`\n * - `vibrate(number[])`: iterate \"on\" segments (even indices) as `tap` pulses\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,\n * or returns `false` when unavailable (matches the spec — browsers that don't\n * support vibration simply return `false`).\n *\n * Caveats (documented in CLAUDE.md as the known lossy trade-off):\n * - SDK haptics are qualitative (\"tickWeak\", \"basicMedium\"), not millisecond\n * durations. The shim approximates intensity from duration but cannot\n * reproduce exact patterns.\n * - Arrays are fired sequentially via `setTimeout`; gaps between pulses are\n * honoured only as \"time until the next tap\", not as silent-vs-vibrating.\n * - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return\n * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\n\nconst BACKUP_KEY = Symbol.for('@ait-co/polyfill/vibrate.original');\nconst HAD_KEY = Symbol.for('@ait-co/polyfill/vibrate.hadOriginal');\n\ninterface BackupHost {\n [BACKUP_KEY]?: ((pattern: VibratePattern) => boolean) | undefined;\n [HAD_KEY]?: boolean;\n}\n\nconst SHORT_VIBRATION_MS = 40;\n\ntype HapticType =\n | 'tickWeak'\n | 'tap'\n | 'tickMedium'\n | 'softMedium'\n | 'basicWeak'\n | 'basicMedium'\n | 'success'\n | 'error'\n | 'wiggle'\n | 'confetti';\n\nasync function haptic(type: HapticType): Promise<void> {\n const sdk = await loadTossSdk();\n const fn = (sdk as { generateHapticFeedback?: unknown } | null)?.generateHapticFeedback;\n if (typeof fn === 'function') {\n try {\n await (fn as (o: { type: HapticType }) => Promise<void>)({ type });\n } catch {\n // Best-effort; spec-level `vibrate` cannot surface errors.\n }\n }\n}\n\nfunction durationToHaptic(duration: number): HapticType {\n return duration < SHORT_VIBRATION_MS ? 'tickWeak' : 'basicMedium';\n}\n\nfunction vibrateShim(pattern: VibratePattern): boolean {\n // Matches the spec: `vibrate(0)` or `vibrate([])` cancels pending vibration.\n // We can't cancel an in-flight SDK haptic (no cancel API), but we still\n // forward the cancel to the browser fallback so native vibration stops.\n const arr = Array.isArray(pattern) ? pattern : [pattern];\n if (arr.length === 0 || arr.every((n) => n === 0)) {\n void (async () => {\n if (!(await isTossEnvironment())) {\n const host = navigator as unknown as BackupHost;\n host[BACKUP_KEY]?.call(navigator, pattern);\n }\n })();\n return true;\n }\n\n void (async () => {\n if (await isTossEnvironment()) {\n if (!Array.isArray(pattern)) {\n await haptic(durationToHaptic(pattern));\n return;\n }\n // Even indices = \"on\" durations, odd indices = pauses. `pattern[i]` is\n // `number | undefined` under `noUncheckedIndexedAccess`; the `undefined`\n // case only arises on out-of-bounds, which our length bound prevents.\n for (let i = 0; i < pattern.length; i += 2) {\n const on = pattern[i];\n if (on === undefined) break;\n if (on > 0) {\n await haptic('tap');\n }\n const pause = pattern[i + 1];\n if (typeof pause === 'number' && pause > 0) {\n await new Promise<void>((r) => setTimeout(r, pause));\n }\n }\n return;\n }\n const host = navigator as unknown as BackupHost;\n const original = host[BACKUP_KEY];\n original?.call(navigator, pattern);\n })();\n\n return true;\n}\n\nexport function installVibrateShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (BACKUP_KEY in host) {\n return () => uninstallVibrateShim();\n }\n\n const nav = navigator as Navigator & { vibrate?: (p: VibratePattern) => boolean };\n host[BACKUP_KEY] = nav.vibrate;\n host[HAD_KEY] = 'vibrate' in nav;\n\n Object.defineProperty(navigator, 'vibrate', {\n value: vibrateShim,\n configurable: true,\n writable: true,\n });\n\n return uninstallVibrateShim;\n}\n\nexport function uninstallVibrateShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(BACKUP_KEY in host)) return;\n\n const original = host[BACKUP_KEY];\n const had = host[HAD_KEY];\n // Prototype-safe restore: delete the instance override first, then only\n // redefine on the instance if the original was an own property the\n // prototype doesn't provide — prevents permanent shadowing of a prototype\n // `vibrate` getter on real browsers.\n delete (navigator as unknown as { vibrate?: (p: VibratePattern) => boolean }).vibrate;\n if (had && navigator.vibrate !== original) {\n Object.defineProperty(navigator, 'vibrate', {\n value: original,\n configurable: true,\n writable: true,\n });\n }\n delete host[BACKUP_KEY];\n delete host[HAD_KEY];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,IAAI;;;;;;;;AAgCJ,eAAsB,oBAAsC;CAG1D,MAAM,QAAQ,WAAW;AACzB,KAAI,UAAU,OAAQ,QAAO;AAC7B,KAAI,UAAU,UAAW,QAAO;AAEhC,KAAI,WAAW,KAAA,EAAW,QAAO;CAEjC,MAAM,MAAM,MAAM,aAAa;AAC/B,KAAI,OAAO,KAAK,yBAAyB,YAAY;AACnD,WAAS;AACT,SAAO;;AAKT,KAAI;EACF,MAAM,UAAU,IAAI,sBAAsB;AAC1C,WAAS,QAAQ,QAAQ,IAAI,OAAO,YAAY;SAC1C;AACN,WAAS;;AAEX,QAAO;;;;;;AAOT,eAAsB,cAA4E;AAChG,KAAI;AACF,SAAO,MAAM,OAAO;SACd;AACN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AC5DX,MAAM,aAAa,OAAO,IAAI,oCAAoC;AAClE,MAAM,UAAU,OAAO,IAAI,uCAAuC;AAOlE,MAAM,qBAAqB;AAc3B,eAAe,OAAO,MAAiC;CAErD,MAAM,MADM,MAAM,aAAa,GACkC;AACjE,KAAI,OAAO,OAAO,WAChB,KAAI;AACF,QAAO,GAAkD,EAAE,MAAM,CAAC;SAC5D;;AAMZ,SAAS,iBAAiB,UAA8B;AACtD,QAAO,WAAW,qBAAqB,aAAa;;AAGtD,SAAS,YAAY,SAAkC;CAIrD,MAAM,MAAM,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ;AACxD,KAAI,IAAI,WAAW,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,EAAE;AACjD,GAAM,YAAY;AAChB,OAAI,CAAE,MAAM,mBAAmB,CAChB,WACR,aAAa,KAAK,WAAW,QAAQ;MAE1C;AACJ,SAAO;;AAGT,EAAM,YAAY;AAChB,MAAI,MAAM,mBAAmB,EAAE;AAC7B,OAAI,CAAC,MAAM,QAAQ,QAAQ,EAAE;AAC3B,UAAM,OAAO,iBAAiB,QAAQ,CAAC;AACvC;;AAKF,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,KAAK,QAAQ;AACnB,QAAI,OAAO,KAAA,EAAW;AACtB,QAAI,KAAK,EACP,OAAM,OAAO,MAAM;IAErB,MAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,OAAO,UAAU,YAAY,QAAQ,EACvC,OAAM,IAAI,SAAe,MAAM,WAAW,GAAG,MAAM,CAAC;;AAGxD;;AAEW,YACS,aACZ,KAAK,WAAW,QAAQ;KAChC;AAEJ,QAAO;;AAGT,SAAgB,qBAAiC;AAC/C,KAAI,OAAO,cAAc,YACvB,cAAa;CAGf,MAAM,OAAO;AACb,KAAI,cAAc,KAChB,cAAa,sBAAsB;CAGrC,MAAM,MAAM;AACZ,MAAK,cAAc,IAAI;AACvB,MAAK,WAAW,aAAa;AAE7B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEF,QAAO;;AAGT,SAAgB,uBAA6B;AAC3C,KAAI,OAAO,cAAc,YAAa;CACtC,MAAM,OAAO;AACb,KAAI,EAAE,cAAc,MAAO;CAE3B,MAAM,WAAW,KAAK;CACtB,MAAM,MAAM,KAAK;AAKjB,QAAQ,UAAsE;AAC9E,KAAI,OAAO,UAAU,YAAY,SAC/B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEJ,QAAO,KAAK;AACZ,QAAO,KAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ait-co/polyfill",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Polyfill so you can build Apps in Toss mini-apps with standard Web APIs (navigator.clipboard, navigator.geolocation, ...) instead of the proprietary SDK",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,12 +36,18 @@
36
36
  "./detect": {
37
37
  "types": "./dist/detect.d.ts",
38
38
  "import": "./dist/detect.js"
39
+ },
40
+ "./auto": {
41
+ "types": "./dist/auto.d.ts",
42
+ "import": "./dist/auto.js"
39
43
  }
40
44
  },
41
45
  "files": [
42
46
  "dist"
43
47
  ],
44
- "sideEffects": false,
48
+ "sideEffects": [
49
+ "./dist/auto.js"
50
+ ],
45
51
  "scripts": {
46
52
  "build": "tsdown",
47
53
  "dev": "tsdown --watch",