tempest-react-sdk 0.38.2 → 0.38.3

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.
Files changed (291) hide show
  1. package/bin/lib/design/collect.mjs +19 -0
  2. package/bin/lib/design/functions.mjs +5 -0
  3. package/bin/lib/design/index.mjs +8 -3
  4. package/bin/lib/design/index.test.mjs +31 -0
  5. package/bin/lib/design/scan.mjs +34 -2
  6. package/bin/lib/design/scan.test.mjs +78 -0
  7. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  8. package/bin/tempest.mjs +90 -6
  9. package/dist/audio/level-meter.cjs.map +1 -1
  10. package/dist/audio/level-meter.js.map +1 -1
  11. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  12. package/dist/audio/use-audio-recorder.js.map +1 -1
  13. package/dist/auth/passkey.cjs.map +1 -1
  14. package/dist/auth/passkey.js.map +1 -1
  15. package/dist/auth/use-passkey.cjs.map +1 -1
  16. package/dist/auth/use-passkey.js.map +1 -1
  17. package/dist/br/BrazilMap.cjs.map +1 -1
  18. package/dist/br/BrazilMap.js.map +1 -1
  19. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  21. package/dist/br/BrazilStateMap.cjs.map +1 -1
  22. package/dist/br/BrazilStateMap.js.map +1 -1
  23. package/dist/br/PixQRCode.cjs.map +1 -1
  24. package/dist/br/PixQRCode.js.map +1 -1
  25. package/dist/br/boleto.cjs.map +1 -1
  26. package/dist/br/boleto.js.map +1 -1
  27. package/dist/br/pix.cjs.map +1 -1
  28. package/dist/br/pix.js.map +1 -1
  29. package/dist/br.d.ts +8 -0
  30. package/dist/capture/media-recorder.cjs.map +1 -1
  31. package/dist/capture/media-recorder.js.map +1 -1
  32. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  33. package/dist/capture/use-barcode-scanner.js.map +1 -1
  34. package/dist/capture/use-screen-capture.cjs.map +1 -1
  35. package/dist/capture/use-screen-capture.js.map +1 -1
  36. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  37. package/dist/capture/use-speech-recognition.js.map +1 -1
  38. package/dist/capture/use-video-recorder.cjs.map +1 -1
  39. package/dist/capture/use-video-recorder.js.map +1 -1
  40. package/dist/charts/types.cjs.map +1 -1
  41. package/dist/charts/types.js.map +1 -1
  42. package/dist/charts.d.ts +8 -0
  43. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  44. package/dist/components/AIChat/AIChat.js.map +1 -1
  45. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  47. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  49. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  50. package/dist/components/AppBar/AppBar.js.map +1 -1
  51. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  53. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  55. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  57. package/dist/components/Button/Button.cjs.map +1 -1
  58. package/dist/components/Button/Button.js.map +1 -1
  59. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  60. package/dist/components/Calendar/Calendar.js.map +1 -1
  61. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  62. package/dist/components/Carousel/Carousel.js.map +1 -1
  63. package/dist/components/Chat/Chat.cjs.map +1 -1
  64. package/dist/components/Chat/Chat.js.map +1 -1
  65. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  67. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  69. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Command/Command.cjs.map +1 -1
  72. package/dist/components/Command/Command.js.map +1 -1
  73. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  75. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  77. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  79. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  80. package/dist/components/DataTable/DataTable.js.map +1 -1
  81. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  83. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  84. package/dist/components/DataTable/EditableCell.js.map +1 -1
  85. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  87. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  89. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  90. package/dist/components/Drawer/Drawer.js.map +1 -1
  91. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  93. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  95. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  97. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  99. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  101. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  103. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  104. package/dist/components/Kanban/Kanban.js.map +1 -1
  105. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  107. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  108. package/dist/components/ListTile/ListTile.js.map +1 -1
  109. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  110. package/dist/components/Markdown/Markdown.js.map +1 -1
  111. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  113. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  114. package/dist/components/Menubar/Menubar.js.map +1 -1
  115. package/dist/components/Modal/Modal.cjs.map +1 -1
  116. package/dist/components/Modal/Modal.js.map +1 -1
  117. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  119. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  121. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  123. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  125. package/dist/components/Page/Page.cjs.map +1 -1
  126. package/dist/components/Page/Page.js.map +1 -1
  127. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  128. package/dist/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  131. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  132. package/dist/components/PinInput/PinInput.js.map +1 -1
  133. package/dist/components/Popover/Popover.cjs.map +1 -1
  134. package/dist/components/Popover/Popover.js.map +1 -1
  135. package/dist/components/Progress/Progress.cjs.map +1 -1
  136. package/dist/components/Progress/Progress.js.map +1 -1
  137. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  138. package/dist/components/QRCode/qr-encode.js.map +1 -1
  139. package/dist/components/Radio/Radio.cjs.map +1 -1
  140. package/dist/components/Radio/Radio.js.map +1 -1
  141. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  143. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  145. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  147. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  148. package/dist/components/Resizable/Resizable.js.map +1 -1
  149. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  151. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  153. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  155. package/dist/components/Slider/Slider.cjs.map +1 -1
  156. package/dist/components/Slider/Slider.js.map +1 -1
  157. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  159. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  161. package/dist/components/Table/Table.cjs.map +1 -1
  162. package/dist/components/Table/Table.js.map +1 -1
  163. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  165. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  166. package/dist/components/Toast/ToastProvider.js.map +1 -1
  167. package/dist/components/Tour/Tour.cjs.map +1 -1
  168. package/dist/components/Tour/Tour.js.map +1 -1
  169. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  170. package/dist/components/Transfer/Transfer.js.map +1 -1
  171. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  172. package/dist/components/TreeView/TreeView.js.map +1 -1
  173. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  175. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  177. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  178. package/dist/components/Wizard/Wizard.js.map +1 -1
  179. package/dist/editor/RichTextEditor.cjs.map +1 -1
  180. package/dist/editor/RichTextEditor.js.map +1 -1
  181. package/dist/forms/FormField.cjs.map +1 -1
  182. package/dist/forms/FormField.js.map +1 -1
  183. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  184. package/dist/geo/TrajectoryMap.js.map +1 -1
  185. package/dist/geo/estimate.cjs.map +1 -1
  186. package/dist/geo/estimate.js.map +1 -1
  187. package/dist/geo/projection.cjs.map +1 -1
  188. package/dist/geo/projection.js.map +1 -1
  189. package/dist/hooks/use-event-listener.cjs.map +1 -1
  190. package/dist/hooks/use-event-listener.js.map +1 -1
  191. package/dist/hooks/use-local-storage.cjs.map +1 -1
  192. package/dist/hooks/use-local-storage.js.map +1 -1
  193. package/dist/hooks/use-sortable.cjs.map +1 -1
  194. package/dist/hooks/use-sortable.js.map +1 -1
  195. package/dist/http/api-client.cjs.map +1 -1
  196. package/dist/http/api-client.js.map +1 -1
  197. package/dist/http/errors.cjs.map +1 -1
  198. package/dist/http/errors.js.map +1 -1
  199. package/dist/http/resumable-upload.cjs.map +1 -1
  200. package/dist/http/resumable-upload.js.map +1 -1
  201. package/dist/http/upload-with-progress.cjs.map +1 -1
  202. package/dist/http/upload-with-progress.js.map +1 -1
  203. package/dist/i18n/I18nProvider.cjs.map +1 -1
  204. package/dist/i18n/I18nProvider.js.map +1 -1
  205. package/dist/imaging/canvas.cjs.map +1 -1
  206. package/dist/imaging/canvas.js.map +1 -1
  207. package/dist/imaging.d.ts +5 -0
  208. package/dist/logger/logger.cjs.map +1 -1
  209. package/dist/logger/logger.js.map +1 -1
  210. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  211. package/dist/oauth/GoogleSignIn.js.map +1 -1
  212. package/dist/offline/create-offline-sync.cjs.map +1 -1
  213. package/dist/offline/create-offline-sync.js.map +1 -1
  214. package/dist/perf/cache-size.cjs +1 -1
  215. package/dist/perf/cache-size.cjs.map +1 -1
  216. package/dist/perf/cache-size.js +27 -6
  217. package/dist/perf/cache-size.js.map +1 -1
  218. package/dist/sse/create-event-stream.cjs.map +1 -1
  219. package/dist/sse/create-event-stream.js.map +1 -1
  220. package/dist/styles.css +1 -1
  221. package/dist/sw/background-sync.cjs.map +1 -1
  222. package/dist/sw/background-sync.js.map +1 -1
  223. package/dist/sw/cache.cjs.map +1 -1
  224. package/dist/sw/cache.js.map +1 -1
  225. package/dist/sw.d.ts +12 -0
  226. package/dist/tabular/compact.cjs.map +1 -1
  227. package/dist/tabular/compact.js.map +1 -1
  228. package/dist/tabular/predictor.cjs.map +1 -1
  229. package/dist/tabular/predictor.js.map +1 -1
  230. package/dist/tabular.d.ts +7 -2
  231. package/dist/tempest-react-sdk.d.ts +133 -6
  232. package/dist/theme/ThemeProvider.cjs.map +1 -1
  233. package/dist/theme/ThemeProvider.js.map +1 -1
  234. package/dist/theme/color.cjs.map +1 -1
  235. package/dist/theme/color.js.map +1 -1
  236. package/dist/theme/create-theme.cjs.map +1 -1
  237. package/dist/theme/create-theme.js.map +1 -1
  238. package/dist/theme/data-viz-ramps.cjs +1 -1
  239. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  240. package/dist/theme/data-viz-ramps.js +1 -1
  241. package/dist/theme/data-viz-ramps.js.map +1 -1
  242. package/dist/utils/storage.cjs.map +1 -1
  243. package/dist/utils/storage.js.map +1 -1
  244. package/dist/vision/core/canvas.cjs.map +1 -1
  245. package/dist/vision/core/canvas.js.map +1 -1
  246. package/dist/vision/core/exceptions.cjs.map +1 -1
  247. package/dist/vision/core/exceptions.js.map +1 -1
  248. package/dist/vision/core/graph.cjs.map +1 -1
  249. package/dist/vision/core/graph.js.map +1 -1
  250. package/dist/vision/core/metadata.cjs.map +1 -1
  251. package/dist/vision/core/metadata.js.map +1 -1
  252. package/dist/vision/core/providers.cjs.map +1 -1
  253. package/dist/vision/core/providers.js.map +1 -1
  254. package/dist/vision/core/session.cjs.map +1 -1
  255. package/dist/vision/core/session.js.map +1 -1
  256. package/dist/vision/core/timing.cjs.map +1 -1
  257. package/dist/vision/core/timing.js.map +1 -1
  258. package/dist/vision/io/image.cjs.map +1 -1
  259. package/dist/vision/io/image.js.map +1 -1
  260. package/dist/vision/labels.cjs.map +1 -1
  261. package/dist/vision/labels.js.map +1 -1
  262. package/dist/vision/luminance.cjs.map +1 -1
  263. package/dist/vision/luminance.js.map +1 -1
  264. package/dist/vision/postprocess/classification.cjs.map +1 -1
  265. package/dist/vision/postprocess/classification.js.map +1 -1
  266. package/dist/vision/postprocess/detection.cjs.map +1 -1
  267. package/dist/vision/postprocess/detection.js.map +1 -1
  268. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  269. package/dist/vision/postprocess/segmentation.js.map +1 -1
  270. package/dist/vision/preprocess/image.cjs.map +1 -1
  271. package/dist/vision/preprocess/image.js.map +1 -1
  272. package/dist/vision/results.cjs.map +1 -1
  273. package/dist/vision/results.js.map +1 -1
  274. package/dist/vision/tasks/base.cjs.map +1 -1
  275. package/dist/vision/tasks/base.js.map +1 -1
  276. package/dist/vision/tasks/classifier.cjs.map +1 -1
  277. package/dist/vision/tasks/classifier.js.map +1 -1
  278. package/dist/vision/tasks/detector.cjs.map +1 -1
  279. package/dist/vision/tasks/detector.js.map +1 -1
  280. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  281. package/dist/vision/tasks/segmenter.js.map +1 -1
  282. package/dist/vision/types.cjs.map +1 -1
  283. package/dist/vision/types.js.map +1 -1
  284. package/dist/vision.d.ts +6 -0
  285. package/dist/vite/tempest-icons.cjs.map +1 -1
  286. package/dist/vite/tempest-icons.js.map +1 -1
  287. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  288. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  289. package/dist/ws/create-web-socket.cjs.map +1 -1
  290. package/dist/ws/create-web-socket.js.map +1 -1
  291. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"segmenter.js","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\n\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { detectionNumClasses, resolveInputSize } from \"../core/graph\";\nimport { modelNames } from \"../core/metadata\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /**\n * Model input `[width, height]` in pixels for letterboxing.\n *\n * Only used when the model's graph leaves its spatial axes dynamic: a graph\n * that declares a static size always wins, since that is the only shape ONNX\n * Runtime will accept. Defaults to `[640, 640]`.\n */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? modelNames(session.metadata) ?? \"coco\", {\n numClasses: options.numClasses ?? detectionNumClasses(session.outputShape) ?? undefined,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n resolveInputSize({\n graphShape: session.inputShape,\n requested: options.inputSize,\n fallback: [640, 640],\n }),\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /**\n * The `[width, height]` this task preprocesses to.\n *\n * Resolved at creation time from the model's graph when it declares a static\n * input, so reading it back tells you the resolution inference really runs at\n * — not merely what was requested.\n */\n get inputSize(): readonly [number, number] {\n return this._inputSize;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n timer.stage(\"inference\");\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n const boxes = this._buildBoxes(detections, orig);\n const masks = this._buildMasks(detections, orig);\n timer.stage(\"postprocess\");\n return [\n new SegmentationResults(\n boxes,\n masks,\n detections,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"segmenter.js","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\n\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { detectionNumClasses, resolveInputSize } from \"../core/graph\";\nimport { modelNames } from \"../core/metadata\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /**\n * Model input `[width, height]` in pixels for letterboxing.\n *\n * Only used when the model's graph leaves its spatial axes dynamic: a graph\n * that declares a static size always wins, since that is the only shape ONNX\n * Runtime will accept. Defaults to `[640, 640]`.\n */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? modelNames(session.metadata) ?? \"coco\", {\n numClasses: options.numClasses ?? detectionNumClasses(session.outputShape) ?? undefined,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n resolveInputSize({\n graphShape: session.inputShape,\n requested: options.inputSize,\n fallback: [640, 640],\n }),\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /**\n * The `[width, height]` this task preprocesses to.\n *\n * Resolved at creation time from the model's graph when it declares a static\n * input, so reading it back tells you the resolution inference really runs at\n * — not merely what was requested.\n */\n get inputSize(): readonly [number, number] {\n return this._inputSize;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n timer.stage(\"inference\");\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n const boxes = this._buildBoxes(detections, orig);\n const masks = this._buildMasks(detections, orig);\n timer.stage(\"postprocess\");\n return [\n new SegmentationResults(\n boxes,\n masks,\n detections,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs","names":[],"sources":["../../src/vision/types.ts"],"sourcesContent":["/**\n * Public output types returned by the SDK's vision tasks.\n *\n * These types form the contract between the SDK and its callers. They mirror\n * the Python `ort-vision-sdk` output dataclasses 1-to-1.\n *\n * Naming is intentionally compatible with the Ultralytics / torchvision idiom\n * (`cls`, `conf`, `box`, `xyxy`, `xywh`, normalized variants) so code ported\n * from those projects keeps working with minimal edits. The original verbose\n * names (`classId`, `className`, `confidence`, `bbox`) are still populated for\n * backwards compatibility.\n */\n\nimport { ImageLoadError } from \"./core/exceptions\";\n\n/**\n * HWC RGB uint8 image — the canonical image format used across the SDK.\n *\n * `data.length` must equal `width * height * 3`. The buffer is laid out row\n * by row, top-to-bottom, with each pixel as `[R, G, B]`.\n */\nexport class RGBImage {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height * 3) {\n throw new ImageLoadError(\n `RGBImage data length ${data.length} does not match width * height * 3 = ${\n width * height * 3\n }.`,\n );\n }\n }\n}\n\n/**\n * Axis-aligned bounding box in absolute pixel coordinates (xyxy format).\n *\n * Coordinates refer to the original input image (before any internal resize),\n * so callers can map detections back onto their source image without any\n * additional bookkeeping.\n */\nexport class BoundingBox {\n constructor(\n public readonly x1: number,\n public readonly y1: number,\n public readonly x2: number,\n public readonly y2: number,\n ) {}\n\n /** Box width in pixels (clamped to non-negative). */\n get width(): number {\n return Math.max(0, this.x2 - this.x1);\n }\n\n /** Box height in pixels (clamped to non-negative). */\n get height(): number {\n return Math.max(0, this.y2 - this.y1);\n }\n\n /** Box area in pixels squared. */\n get area(): number {\n return this.width * this.height;\n }\n\n /** The box as `[x1, y1, x2, y2]` in absolute pixels (Ultralytics-style). */\n get xyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * The box as `[cx, cy, w, h]` with `(cx, cy)` at the center.\n *\n * Matches Ultralytics' `boxes.xywh` and YOLO's native head format. For the\n * top-left `[x, y, w, h]` convention, use {@link asXywh}.\n */\n get xywh(): readonly [number, number, number, number] {\n return [(this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2, this.width, this.height];\n }\n\n /**\n * The box as `[x1, y1, x2, y2]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xyxyn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n return [this.x1 / w, this.y1 / h, this.x2 / w, this.y2 / h];\n }\n\n /**\n * The box as `[cx, cy, w, h]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xywhn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n const [cx, cy, bw, bh] = this.xywh;\n return [cx / w, cy / h, bw / w, bh / h];\n }\n\n /** Returns `[x1, y1, x2, y2]`. */\n asXyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * Returns `[x, y, width, height]` with `(x, y)` at the **top-left**.\n *\n * Note: this is the top-left convention. Ultralytics' `xywh` getter uses\n * **center** coordinates — for that, read {@link xywh}.\n */\n asXywh(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.width, this.height];\n }\n\n /** Returns `[x1, y1, x2, y2]` truncated to integers, useful for slicing arrays. */\n asIntXyxy(): readonly [number, number, number, number] {\n return [Math.trunc(this.x1), Math.trunc(this.y1), Math.trunc(this.x2), Math.trunc(this.y2)];\n }\n}\n\n/**\n * Probability assigned to a single class for a classification prediction.\n *\n * `cls` / `name` / `conf` are Ultralytics-style aliases populated alongside\n * the verbose `classId` / `className` / `probability` fields.\n */\nexport interface ClassProbability {\n readonly classId: number;\n readonly className: string;\n readonly probability: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `probability` (Ultralytics-style). */\n readonly conf: number;\n}\n\n/**\n * Output of an image classification inference.\n */\nexport interface ClassificationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** The original input image as an HWC RGB uint8 array. */\n readonly image: RGBImage;\n /**\n * Probabilities per class, sorted in descending order. The first entry\n * mirrors `classId`, `className`, and `confidence`. When `topK` was passed\n * to `predict`, the array is truncated to that length.\n */\n readonly probabilities: readonly ClassProbability[];\n}\n\n/**\n * Single detected object produced by an object-detection model.\n */\nexport interface DetectionResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * The original image cropped to `bbox`, HWC RGB uint8. Empty boxes\n * (zero area) yield a zero-sized `RGBImage`.\n */\n readonly croppedImage: RGBImage;\n}\n\n/**\n * Single-channel binary or grayscale mask, laid out row-major.\n *\n * `data.length` must equal `width * height`. For binary masks, values are\n * `0` (background) or `255` (foreground); soft masks may use the full\n * `[0, 255]` range.\n */\nexport class Mask {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height) {\n throw new ImageLoadError(\n `Mask data length ${data.length} does not match width * height = ${\n width * height\n }.`,\n );\n }\n }\n}\n\n/**\n * Single segmented instance produced by an instance-segmentation model.\n *\n * Mirrors {@link DetectionResult} and adds the per-instance binary mask\n * plus a \"ready-to-display\" background-removed crop.\n */\nexport interface SegmentationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * Binary mask cropped to `bbox`. Values are `0` (background) or `255`\n * (foreground). Empty boxes yield a zero-sized `Mask`.\n */\n readonly mask: Mask;\n /**\n * The original image cropped to `bbox` with background pixels (where\n * `mask.data[i] === 0`) zeroed out. Empty boxes yield a zero-sized\n * `RGBImage`.\n */\n readonly segmentedImage: RGBImage;\n}\n"],"mappings":"yCAqBA,IAAa,EAAb,KAAsB,CAEE,KACA,MACA,OAHpB,YACI,EACA,EACA,EACF,CACE,GAJgB,KAAA,KAAA,EACA,KAAA,MAAA,EACA,KAAA,OAAA,EAEZ,EAAK,SAAW,EAAQ,EAAS,EACjC,MAAM,IAAI,EAAA,eACN,wBAAwB,EAAK,OAAO,uCAChC,EAAQ,EAAS,EACpB,EACL,CAER,CACJ,EASa,EAAb,KAAyB,CAED,GACA,GACA,GACA,GAJpB,YACI,EACA,EACA,EACA,EACF,CAJkB,KAAA,GAAA,EACA,KAAA,GAAA,EACA,KAAA,GAAA,EACA,KAAA,GAAA,CACjB,CAGH,IAAI,OAAgB,CAChB,OAAO,KAAK,IAAI,EAAG,KAAK,GAAK,KAAK,EAAE,CACxC,CAGA,IAAI,QAAiB,CACjB,OAAO,KAAK,IAAI,EAAG,KAAK,GAAK,KAAK,EAAE,CACxC,CAGA,IAAI,MAAe,CACf,OAAO,KAAK,MAAQ,KAAK,MAC7B,CAGA,IAAI,MAAkD,CAClD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,GAAI,KAAK,EAAE,CAC9C,CAQA,IAAI,MAAkD,CAClD,MAAO,EAAE,KAAK,GAAK,KAAK,IAAM,GAAI,KAAK,GAAK,KAAK,IAAM,EAAG,KAAK,MAAO,KAAK,MAAM,CACrF,CAOA,MAAM,EAAiF,CACnF,GAAM,CAAC,EAAG,GAAK,EAEf,OADI,GAAK,GAAK,GAAK,EAAU,CAAC,EAAG,EAAG,EAAG,CAAC,EACjC,CAAC,KAAK,GAAK,EAAG,KAAK,GAAK,EAAG,KAAK,GAAK,EAAG,KAAK,GAAK,CAAC,CAC9D,CAOA,MAAM,EAAiF,CACnF,GAAM,CAAC,EAAG,GAAK,EACf,GAAI,GAAK,GAAK,GAAK,EAAG,MAAO,CAAC,EAAG,EAAG,EAAG,CAAC,EACxC,GAAM,CAAC,EAAI,EAAI,EAAI,GAAM,KAAK,KAC9B,MAAO,CAAC,EAAK,EAAG,EAAK,EAAG,EAAK,EAAG,EAAK,CAAC,CAC1C,CAGA,QAAoD,CAChD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,GAAI,KAAK,EAAE,CAC9C,CAQA,QAAoD,CAChD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,MAAO,KAAK,MAAM,CACrD,CAGA,WAAuD,CACnD,MAAO,CAAC,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,CAAC,CAC9F,CACJ,EAyEa,EAAb,KAAkB,CAEM,KACA,MACA,OAHpB,YACI,EACA,EACA,EACF,CACE,GAJgB,KAAA,KAAA,EACA,KAAA,MAAA,EACA,KAAA,OAAA,EAEZ,EAAK,SAAW,EAAQ,EACxB,MAAM,IAAI,EAAA,eACN,oBAAoB,EAAK,OAAO,mCAC5B,EAAQ,EACX,EACL,CAER,CACJ"}
1
+ {"version":3,"file":"types.cjs","names":[],"sources":["../../src/vision/types.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Public output types returned by the SDK's vision tasks.\n *\n * These types form the contract between the SDK and its callers. They mirror\n * the Python `ort-vision-sdk` output dataclasses 1-to-1.\n *\n * Naming is intentionally compatible with the Ultralytics / torchvision idiom\n * (`cls`, `conf`, `box`, `xyxy`, `xywh`, normalized variants) so code ported\n * from those projects keeps working with minimal edits. The original verbose\n * names (`classId`, `className`, `confidence`, `bbox`) are still populated for\n * backwards compatibility.\n */\n\nimport { ImageLoadError } from \"./core/exceptions\";\n\n/**\n * HWC RGB uint8 image — the canonical image format used across the SDK.\n *\n * `data.length` must equal `width * height * 3`. The buffer is laid out row\n * by row, top-to-bottom, with each pixel as `[R, G, B]`.\n */\nexport class RGBImage {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height * 3) {\n throw new ImageLoadError(\n `RGBImage data length ${data.length} does not match width * height * 3 = ${\n width * height * 3\n }.`,\n );\n }\n }\n}\n\n/**\n * Axis-aligned bounding box in absolute pixel coordinates (xyxy format).\n *\n * Coordinates refer to the original input image (before any internal resize),\n * so callers can map detections back onto their source image without any\n * additional bookkeeping.\n */\nexport class BoundingBox {\n constructor(\n public readonly x1: number,\n public readonly y1: number,\n public readonly x2: number,\n public readonly y2: number,\n ) {}\n\n /** Box width in pixels (clamped to non-negative). */\n get width(): number {\n return Math.max(0, this.x2 - this.x1);\n }\n\n /** Box height in pixels (clamped to non-negative). */\n get height(): number {\n return Math.max(0, this.y2 - this.y1);\n }\n\n /** Box area in pixels squared. */\n get area(): number {\n return this.width * this.height;\n }\n\n /** The box as `[x1, y1, x2, y2]` in absolute pixels (Ultralytics-style). */\n get xyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * The box as `[cx, cy, w, h]` with `(cx, cy)` at the center.\n *\n * Matches Ultralytics' `boxes.xywh` and YOLO's native head format. For the\n * top-left `[x, y, w, h]` convention, use {@link asXywh}.\n */\n get xywh(): readonly [number, number, number, number] {\n return [(this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2, this.width, this.height];\n }\n\n /**\n * The box as `[x1, y1, x2, y2]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xyxyn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n return [this.x1 / w, this.y1 / h, this.x2 / w, this.y2 / h];\n }\n\n /**\n * The box as `[cx, cy, w, h]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xywhn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n const [cx, cy, bw, bh] = this.xywh;\n return [cx / w, cy / h, bw / w, bh / h];\n }\n\n /** Returns `[x1, y1, x2, y2]`. */\n asXyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * Returns `[x, y, width, height]` with `(x, y)` at the **top-left**.\n *\n * Note: this is the top-left convention. Ultralytics' `xywh` getter uses\n * **center** coordinates — for that, read {@link xywh}.\n */\n asXywh(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.width, this.height];\n }\n\n /** Returns `[x1, y1, x2, y2]` truncated to integers, useful for slicing arrays. */\n asIntXyxy(): readonly [number, number, number, number] {\n return [Math.trunc(this.x1), Math.trunc(this.y1), Math.trunc(this.x2), Math.trunc(this.y2)];\n }\n}\n\n/**\n * Probability assigned to a single class for a classification prediction.\n *\n * `cls` / `name` / `conf` are Ultralytics-style aliases populated alongside\n * the verbose `classId` / `className` / `probability` fields.\n */\nexport interface ClassProbability {\n readonly classId: number;\n readonly className: string;\n readonly probability: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `probability` (Ultralytics-style). */\n readonly conf: number;\n}\n\n/**\n * Output of an image classification inference.\n */\nexport interface ClassificationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** The original input image as an HWC RGB uint8 array. */\n readonly image: RGBImage;\n /**\n * Probabilities per class, sorted in descending order. The first entry\n * mirrors `classId`, `className`, and `confidence`. When `topK` was passed\n * to `predict`, the array is truncated to that length.\n */\n readonly probabilities: readonly ClassProbability[];\n}\n\n/**\n * Single detected object produced by an object-detection model.\n */\nexport interface DetectionResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * The original image cropped to `bbox`, HWC RGB uint8. Empty boxes\n * (zero area) yield a zero-sized `RGBImage`.\n */\n readonly croppedImage: RGBImage;\n}\n\n/**\n * Single-channel binary or grayscale mask, laid out row-major.\n *\n * `data.length` must equal `width * height`. For binary masks, values are\n * `0` (background) or `255` (foreground); soft masks may use the full\n * `[0, 255]` range.\n */\nexport class Mask {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height) {\n throw new ImageLoadError(\n `Mask data length ${data.length} does not match width * height = ${\n width * height\n }.`,\n );\n }\n }\n}\n\n/**\n * Single segmented instance produced by an instance-segmentation model.\n *\n * Mirrors {@link DetectionResult} and adds the per-instance binary mask\n * plus a \"ready-to-display\" background-removed crop.\n */\nexport interface SegmentationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * Binary mask cropped to `bbox`. Values are `0` (background) or `255`\n * (foreground). Empty boxes yield a zero-sized `Mask`.\n */\n readonly mask: Mask;\n /**\n * The original image cropped to `bbox` with background pixels (where\n * `mask.data[i] === 0`) zeroed out. Empty boxes yield a zero-sized\n * `RGBImage`.\n */\n readonly segmentedImage: RGBImage;\n}\n"],"mappings":"yCAsBA,IAAa,EAAb,KAAsB,CAEE,KACA,MACA,OAHpB,YACI,EACA,EACA,EACF,CACE,GAJgB,KAAA,KAAA,EACA,KAAA,MAAA,EACA,KAAA,OAAA,EAEZ,EAAK,SAAW,EAAQ,EAAS,EACjC,MAAM,IAAI,EAAA,eACN,wBAAwB,EAAK,OAAO,uCAChC,EAAQ,EAAS,EACpB,EACL,CAER,CACJ,EASa,EAAb,KAAyB,CAED,GACA,GACA,GACA,GAJpB,YACI,EACA,EACA,EACA,EACF,CAJkB,KAAA,GAAA,EACA,KAAA,GAAA,EACA,KAAA,GAAA,EACA,KAAA,GAAA,CACjB,CAGH,IAAI,OAAgB,CAChB,OAAO,KAAK,IAAI,EAAG,KAAK,GAAK,KAAK,EAAE,CACxC,CAGA,IAAI,QAAiB,CACjB,OAAO,KAAK,IAAI,EAAG,KAAK,GAAK,KAAK,EAAE,CACxC,CAGA,IAAI,MAAe,CACf,OAAO,KAAK,MAAQ,KAAK,MAC7B,CAGA,IAAI,MAAkD,CAClD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,GAAI,KAAK,EAAE,CAC9C,CAQA,IAAI,MAAkD,CAClD,MAAO,EAAE,KAAK,GAAK,KAAK,IAAM,GAAI,KAAK,GAAK,KAAK,IAAM,EAAG,KAAK,MAAO,KAAK,MAAM,CACrF,CAOA,MAAM,EAAiF,CACnF,GAAM,CAAC,EAAG,GAAK,EAEf,OADI,GAAK,GAAK,GAAK,EAAU,CAAC,EAAG,EAAG,EAAG,CAAC,EACjC,CAAC,KAAK,GAAK,EAAG,KAAK,GAAK,EAAG,KAAK,GAAK,EAAG,KAAK,GAAK,CAAC,CAC9D,CAOA,MAAM,EAAiF,CACnF,GAAM,CAAC,EAAG,GAAK,EACf,GAAI,GAAK,GAAK,GAAK,EAAG,MAAO,CAAC,EAAG,EAAG,EAAG,CAAC,EACxC,GAAM,CAAC,EAAI,EAAI,EAAI,GAAM,KAAK,KAC9B,MAAO,CAAC,EAAK,EAAG,EAAK,EAAG,EAAK,EAAG,EAAK,CAAC,CAC1C,CAGA,QAAoD,CAChD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,GAAI,KAAK,EAAE,CAC9C,CAQA,QAAoD,CAChD,MAAO,CAAC,KAAK,GAAI,KAAK,GAAI,KAAK,MAAO,KAAK,MAAM,CACrD,CAGA,WAAuD,CACnD,MAAO,CAAC,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,EAAG,KAAK,MAAM,KAAK,EAAE,CAAC,CAC9F,CACJ,EAyEa,EAAb,KAAkB,CAEM,KACA,MACA,OAHpB,YACI,EACA,EACA,EACF,CACE,GAJgB,KAAA,KAAA,EACA,KAAA,MAAA,EACA,KAAA,OAAA,EAEZ,EAAK,SAAW,EAAQ,EACxB,MAAM,IAAI,EAAA,eACN,oBAAoB,EAAK,OAAO,mCAC5B,EAAQ,EACX,EACL,CAER,CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/vision/types.ts"],"sourcesContent":["/**\n * Public output types returned by the SDK's vision tasks.\n *\n * These types form the contract between the SDK and its callers. They mirror\n * the Python `ort-vision-sdk` output dataclasses 1-to-1.\n *\n * Naming is intentionally compatible with the Ultralytics / torchvision idiom\n * (`cls`, `conf`, `box`, `xyxy`, `xywh`, normalized variants) so code ported\n * from those projects keeps working with minimal edits. The original verbose\n * names (`classId`, `className`, `confidence`, `bbox`) are still populated for\n * backwards compatibility.\n */\n\nimport { ImageLoadError } from \"./core/exceptions\";\n\n/**\n * HWC RGB uint8 image — the canonical image format used across the SDK.\n *\n * `data.length` must equal `width * height * 3`. The buffer is laid out row\n * by row, top-to-bottom, with each pixel as `[R, G, B]`.\n */\nexport class RGBImage {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height * 3) {\n throw new ImageLoadError(\n `RGBImage data length ${data.length} does not match width * height * 3 = ${\n width * height * 3\n }.`,\n );\n }\n }\n}\n\n/**\n * Axis-aligned bounding box in absolute pixel coordinates (xyxy format).\n *\n * Coordinates refer to the original input image (before any internal resize),\n * so callers can map detections back onto their source image without any\n * additional bookkeeping.\n */\nexport class BoundingBox {\n constructor(\n public readonly x1: number,\n public readonly y1: number,\n public readonly x2: number,\n public readonly y2: number,\n ) {}\n\n /** Box width in pixels (clamped to non-negative). */\n get width(): number {\n return Math.max(0, this.x2 - this.x1);\n }\n\n /** Box height in pixels (clamped to non-negative). */\n get height(): number {\n return Math.max(0, this.y2 - this.y1);\n }\n\n /** Box area in pixels squared. */\n get area(): number {\n return this.width * this.height;\n }\n\n /** The box as `[x1, y1, x2, y2]` in absolute pixels (Ultralytics-style). */\n get xyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * The box as `[cx, cy, w, h]` with `(cx, cy)` at the center.\n *\n * Matches Ultralytics' `boxes.xywh` and YOLO's native head format. For the\n * top-left `[x, y, w, h]` convention, use {@link asXywh}.\n */\n get xywh(): readonly [number, number, number, number] {\n return [(this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2, this.width, this.height];\n }\n\n /**\n * The box as `[x1, y1, x2, y2]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xyxyn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n return [this.x1 / w, this.y1 / h, this.x2 / w, this.y2 / h];\n }\n\n /**\n * The box as `[cx, cy, w, h]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xywhn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n const [cx, cy, bw, bh] = this.xywh;\n return [cx / w, cy / h, bw / w, bh / h];\n }\n\n /** Returns `[x1, y1, x2, y2]`. */\n asXyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * Returns `[x, y, width, height]` with `(x, y)` at the **top-left**.\n *\n * Note: this is the top-left convention. Ultralytics' `xywh` getter uses\n * **center** coordinates — for that, read {@link xywh}.\n */\n asXywh(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.width, this.height];\n }\n\n /** Returns `[x1, y1, x2, y2]` truncated to integers, useful for slicing arrays. */\n asIntXyxy(): readonly [number, number, number, number] {\n return [Math.trunc(this.x1), Math.trunc(this.y1), Math.trunc(this.x2), Math.trunc(this.y2)];\n }\n}\n\n/**\n * Probability assigned to a single class for a classification prediction.\n *\n * `cls` / `name` / `conf` are Ultralytics-style aliases populated alongside\n * the verbose `classId` / `className` / `probability` fields.\n */\nexport interface ClassProbability {\n readonly classId: number;\n readonly className: string;\n readonly probability: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `probability` (Ultralytics-style). */\n readonly conf: number;\n}\n\n/**\n * Output of an image classification inference.\n */\nexport interface ClassificationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** The original input image as an HWC RGB uint8 array. */\n readonly image: RGBImage;\n /**\n * Probabilities per class, sorted in descending order. The first entry\n * mirrors `classId`, `className`, and `confidence`. When `topK` was passed\n * to `predict`, the array is truncated to that length.\n */\n readonly probabilities: readonly ClassProbability[];\n}\n\n/**\n * Single detected object produced by an object-detection model.\n */\nexport interface DetectionResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * The original image cropped to `bbox`, HWC RGB uint8. Empty boxes\n * (zero area) yield a zero-sized `RGBImage`.\n */\n readonly croppedImage: RGBImage;\n}\n\n/**\n * Single-channel binary or grayscale mask, laid out row-major.\n *\n * `data.length` must equal `width * height`. For binary masks, values are\n * `0` (background) or `255` (foreground); soft masks may use the full\n * `[0, 255]` range.\n */\nexport class Mask {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height) {\n throw new ImageLoadError(\n `Mask data length ${data.length} does not match width * height = ${\n width * height\n }.`,\n );\n }\n }\n}\n\n/**\n * Single segmented instance produced by an instance-segmentation model.\n *\n * Mirrors {@link DetectionResult} and adds the per-instance binary mask\n * plus a \"ready-to-display\" background-removed crop.\n */\nexport interface SegmentationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * Binary mask cropped to `bbox`. Values are `0` (background) or `255`\n * (foreground). Empty boxes yield a zero-sized `Mask`.\n */\n readonly mask: Mask;\n /**\n * The original image cropped to `bbox` with background pixels (where\n * `mask.data[i] === 0`) zeroed out. Empty boxes yield a zero-sized\n * `RGBImage`.\n */\n readonly segmentedImage: RGBImage;\n}\n"],"mappings":";;AAqBA,IAAa,IAAb,MAAsB;CAEE;CACA;CACA;CAHpB,YACI,GACA,GACA,GACF;EACE,IAJgB,KAAA,OAAA,GACA,KAAA,QAAA,GACA,KAAA,SAAA,GAEZ,EAAK,WAAW,IAAQ,IAAS,GACjC,MAAM,IAAI,EACN,wBAAwB,EAAK,OAAO,uCAChC,IAAQ,IAAS,EACpB,EACL;CAER;AACJ,GASa,IAAb,MAAyB;CAED;CACA;CACA;CACA;CAJpB,YACI,GACA,GACA,GACA,GACF;EADkB,AAHA,KAAA,KAAA,GACA,KAAA,KAAA,GACA,KAAA,KAAA,GACA,KAAA,KAAA;CACjB;CAGH,IAAI,QAAgB;EAChB,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,EAAE;CACxC;CAGA,IAAI,SAAiB;EACjB,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,EAAE;CACxC;CAGA,IAAI,OAAe;EACf,OAAO,KAAK,QAAQ,KAAK;CAC7B;CAGA,IAAI,OAAkD;EAClD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAI,KAAK;EAAE;CAC9C;CAQA,IAAI,OAAkD;EAClD,OAAO;IAAE,KAAK,KAAK,KAAK,MAAM;IAAI,KAAK,KAAK,KAAK,MAAM;GAAG,KAAK;GAAO,KAAK;EAAM;CACrF;CAOA,MAAM,GAAiF;EACnF,IAAM,CAAC,GAAG,KAAK;EAEf,OADI,KAAK,KAAK,KAAK,IAAU;GAAC;GAAG;GAAG;GAAG;EAAC,IACjC;GAAC,KAAK,KAAK;GAAG,KAAK,KAAK;GAAG,KAAK,KAAK;GAAG,KAAK,KAAK;EAAC;CAC9D;CAOA,MAAM,GAAiF;EACnF,IAAM,CAAC,GAAG,KAAK;EACf,IAAI,KAAK,KAAK,KAAK,GAAG,OAAO;GAAC;GAAG;GAAG;GAAG;EAAC;EACxC,IAAM,CAAC,GAAI,GAAI,GAAI,KAAM,KAAK;EAC9B,OAAO;GAAC,IAAK;GAAG,IAAK;GAAG,IAAK;GAAG,IAAK;EAAC;CAC1C;CAGA,SAAoD;EAChD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAI,KAAK;EAAE;CAC9C;CAQA,SAAoD;EAChD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAO,KAAK;EAAM;CACrD;CAGA,YAAuD;EACnD,OAAO;GAAC,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;EAAC;CAC9F;AACJ,GAyEa,IAAb,MAAkB;CAEM;CACA;CACA;CAHpB,YACI,GACA,GACA,GACF;EACE,IAJgB,KAAA,OAAA,GACA,KAAA,QAAA,GACA,KAAA,SAAA,GAEZ,EAAK,WAAW,IAAQ,GACxB,MAAM,IAAI,EACN,oBAAoB,EAAK,OAAO,mCAC5B,IAAQ,EACX,EACL;CAER;AACJ"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/vision/types.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Public output types returned by the SDK's vision tasks.\n *\n * These types form the contract between the SDK and its callers. They mirror\n * the Python `ort-vision-sdk` output dataclasses 1-to-1.\n *\n * Naming is intentionally compatible with the Ultralytics / torchvision idiom\n * (`cls`, `conf`, `box`, `xyxy`, `xywh`, normalized variants) so code ported\n * from those projects keeps working with minimal edits. The original verbose\n * names (`classId`, `className`, `confidence`, `bbox`) are still populated for\n * backwards compatibility.\n */\n\nimport { ImageLoadError } from \"./core/exceptions\";\n\n/**\n * HWC RGB uint8 image — the canonical image format used across the SDK.\n *\n * `data.length` must equal `width * height * 3`. The buffer is laid out row\n * by row, top-to-bottom, with each pixel as `[R, G, B]`.\n */\nexport class RGBImage {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height * 3) {\n throw new ImageLoadError(\n `RGBImage data length ${data.length} does not match width * height * 3 = ${\n width * height * 3\n }.`,\n );\n }\n }\n}\n\n/**\n * Axis-aligned bounding box in absolute pixel coordinates (xyxy format).\n *\n * Coordinates refer to the original input image (before any internal resize),\n * so callers can map detections back onto their source image without any\n * additional bookkeeping.\n */\nexport class BoundingBox {\n constructor(\n public readonly x1: number,\n public readonly y1: number,\n public readonly x2: number,\n public readonly y2: number,\n ) {}\n\n /** Box width in pixels (clamped to non-negative). */\n get width(): number {\n return Math.max(0, this.x2 - this.x1);\n }\n\n /** Box height in pixels (clamped to non-negative). */\n get height(): number {\n return Math.max(0, this.y2 - this.y1);\n }\n\n /** Box area in pixels squared. */\n get area(): number {\n return this.width * this.height;\n }\n\n /** The box as `[x1, y1, x2, y2]` in absolute pixels (Ultralytics-style). */\n get xyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * The box as `[cx, cy, w, h]` with `(cx, cy)` at the center.\n *\n * Matches Ultralytics' `boxes.xywh` and YOLO's native head format. For the\n * top-left `[x, y, w, h]` convention, use {@link asXywh}.\n */\n get xywh(): readonly [number, number, number, number] {\n return [(this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2, this.width, this.height];\n }\n\n /**\n * The box as `[x1, y1, x2, y2]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xyxyn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n return [this.x1 / w, this.y1 / h, this.x2 / w, this.y2 / h];\n }\n\n /**\n * The box as `[cx, cy, w, h]` normalized to `[0, 1]`.\n *\n * @param origShape `[height, width]` of the source image, in pixels.\n */\n xywhn(origShape: readonly [number, number]): readonly [number, number, number, number] {\n const [h, w] = origShape;\n if (w <= 0 || h <= 0) return [0, 0, 0, 0];\n const [cx, cy, bw, bh] = this.xywh;\n return [cx / w, cy / h, bw / w, bh / h];\n }\n\n /** Returns `[x1, y1, x2, y2]`. */\n asXyxy(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.x2, this.y2];\n }\n\n /**\n * Returns `[x, y, width, height]` with `(x, y)` at the **top-left**.\n *\n * Note: this is the top-left convention. Ultralytics' `xywh` getter uses\n * **center** coordinates — for that, read {@link xywh}.\n */\n asXywh(): readonly [number, number, number, number] {\n return [this.x1, this.y1, this.width, this.height];\n }\n\n /** Returns `[x1, y1, x2, y2]` truncated to integers, useful for slicing arrays. */\n asIntXyxy(): readonly [number, number, number, number] {\n return [Math.trunc(this.x1), Math.trunc(this.y1), Math.trunc(this.x2), Math.trunc(this.y2)];\n }\n}\n\n/**\n * Probability assigned to a single class for a classification prediction.\n *\n * `cls` / `name` / `conf` are Ultralytics-style aliases populated alongside\n * the verbose `classId` / `className` / `probability` fields.\n */\nexport interface ClassProbability {\n readonly classId: number;\n readonly className: string;\n readonly probability: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `probability` (Ultralytics-style). */\n readonly conf: number;\n}\n\n/**\n * Output of an image classification inference.\n */\nexport interface ClassificationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** The original input image as an HWC RGB uint8 array. */\n readonly image: RGBImage;\n /**\n * Probabilities per class, sorted in descending order. The first entry\n * mirrors `classId`, `className`, and `confidence`. When `topK` was passed\n * to `predict`, the array is truncated to that length.\n */\n readonly probabilities: readonly ClassProbability[];\n}\n\n/**\n * Single detected object produced by an object-detection model.\n */\nexport interface DetectionResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * The original image cropped to `bbox`, HWC RGB uint8. Empty boxes\n * (zero area) yield a zero-sized `RGBImage`.\n */\n readonly croppedImage: RGBImage;\n}\n\n/**\n * Single-channel binary or grayscale mask, laid out row-major.\n *\n * `data.length` must equal `width * height`. For binary masks, values are\n * `0` (background) or `255` (foreground); soft masks may use the full\n * `[0, 255]` range.\n */\nexport class Mask {\n constructor(\n public readonly data: Uint8Array,\n public readonly width: number,\n public readonly height: number,\n ) {\n if (data.length !== width * height) {\n throw new ImageLoadError(\n `Mask data length ${data.length} does not match width * height = ${\n width * height\n }.`,\n );\n }\n }\n}\n\n/**\n * Single segmented instance produced by an instance-segmentation model.\n *\n * Mirrors {@link DetectionResult} and adds the per-instance binary mask\n * plus a \"ready-to-display\" background-removed crop.\n */\nexport interface SegmentationResult {\n readonly classId: number;\n readonly className: string;\n readonly confidence: number;\n readonly bbox: BoundingBox;\n /** Alias for `classId` (Ultralytics-style). */\n readonly cls: number;\n /** Alias for `className`. */\n readonly name: string;\n /** Alias for `confidence` (Ultralytics-style). */\n readonly conf: number;\n /** Alias for `bbox` (Ultralytics-style). */\n readonly box: BoundingBox;\n /**\n * Binary mask cropped to `bbox`. Values are `0` (background) or `255`\n * (foreground). Empty boxes yield a zero-sized `Mask`.\n */\n readonly mask: Mask;\n /**\n * The original image cropped to `bbox` with background pixels (where\n * `mask.data[i] === 0`) zeroed out. Empty boxes yield a zero-sized\n * `RGBImage`.\n */\n readonly segmentedImage: RGBImage;\n}\n"],"mappings":";;AAsBA,IAAa,IAAb,MAAsB;CAEE;CACA;CACA;CAHpB,YACI,GACA,GACA,GACF;EACE,IAJgB,KAAA,OAAA,GACA,KAAA,QAAA,GACA,KAAA,SAAA,GAEZ,EAAK,WAAW,IAAQ,IAAS,GACjC,MAAM,IAAI,EACN,wBAAwB,EAAK,OAAO,uCAChC,IAAQ,IAAS,EACpB,EACL;CAER;AACJ,GASa,IAAb,MAAyB;CAED;CACA;CACA;CACA;CAJpB,YACI,GACA,GACA,GACA,GACF;EADkB,AAHA,KAAA,KAAA,GACA,KAAA,KAAA,GACA,KAAA,KAAA,GACA,KAAA,KAAA;CACjB;CAGH,IAAI,QAAgB;EAChB,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,EAAE;CACxC;CAGA,IAAI,SAAiB;EACjB,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,EAAE;CACxC;CAGA,IAAI,OAAe;EACf,OAAO,KAAK,QAAQ,KAAK;CAC7B;CAGA,IAAI,OAAkD;EAClD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAI,KAAK;EAAE;CAC9C;CAQA,IAAI,OAAkD;EAClD,OAAO;IAAE,KAAK,KAAK,KAAK,MAAM;IAAI,KAAK,KAAK,KAAK,MAAM;GAAG,KAAK;GAAO,KAAK;EAAM;CACrF;CAOA,MAAM,GAAiF;EACnF,IAAM,CAAC,GAAG,KAAK;EAEf,OADI,KAAK,KAAK,KAAK,IAAU;GAAC;GAAG;GAAG;GAAG;EAAC,IACjC;GAAC,KAAK,KAAK;GAAG,KAAK,KAAK;GAAG,KAAK,KAAK;GAAG,KAAK,KAAK;EAAC;CAC9D;CAOA,MAAM,GAAiF;EACnF,IAAM,CAAC,GAAG,KAAK;EACf,IAAI,KAAK,KAAK,KAAK,GAAG,OAAO;GAAC;GAAG;GAAG;GAAG;EAAC;EACxC,IAAM,CAAC,GAAI,GAAI,GAAI,KAAM,KAAK;EAC9B,OAAO;GAAC,IAAK;GAAG,IAAK;GAAG,IAAK;GAAG,IAAK;EAAC;CAC1C;CAGA,SAAoD;EAChD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAI,KAAK;EAAE;CAC9C;CAQA,SAAoD;EAChD,OAAO;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAO,KAAK;EAAM;CACrD;CAGA,YAAuD;EACnD,OAAO;GAAC,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;GAAG,KAAK,MAAM,KAAK,EAAE;EAAC;CAC9F;AACJ,GAyEa,IAAb,MAAkB;CAEM;CACA;CACA;CAHpB,YACI,GACA,GACA,GACF;EACE,IAJgB,KAAA,OAAA,GACA,KAAA,QAAA,GACA,KAAA,SAAA,GAEZ,EAAK,WAAW,IAAQ,GACxB,MAAM,IAAI,EACN,oBAAoB,EAAK,OAAO,mCAC5B,IAAQ,EACX,EACL;CAER;AACJ"}
package/dist/vision.d.ts CHANGED
@@ -689,6 +689,7 @@ export declare function isLuminanceAcceptable(luminance: number, threshold: numb
689
689
  export declare class LabelMapError extends OrtVisionError {
690
690
  }
691
691
 
692
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
692
693
  /**
693
694
  * Class label resolution: presets, lists, dicts, or auto-generated.
694
695
  *
@@ -982,6 +983,7 @@ export declare interface LetterboxResult {
982
983
  readonly readMetadata?: boolean;
983
984
  }
984
985
 
986
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
985
987
  /**
986
988
  * Exceptions raised by the SDK.
987
989
  *
@@ -1020,6 +1022,7 @@ export declare interface LetterboxResult {
1020
1022
  export declare class ProviderNotAvailableError extends OrtVisionError {
1021
1023
  }
1022
1024
 
1025
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
1023
1026
  /**
1024
1027
  * Read the metadata an exporter baked into a `.onnx` file.
1025
1028
  *
@@ -1096,6 +1099,7 @@ export declare interface LetterboxResult {
1096
1099
  */
1097
1100
  export declare function resolveProviders(requested?: readonly string[]): string[];
1098
1101
 
1102
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
1099
1103
  /**
1100
1104
  * Public output types returned by the SDK's vision tasks.
1101
1105
  *
@@ -1290,6 +1294,7 @@ export declare interface LetterboxResult {
1290
1294
  readonly classes?: readonly number[];
1291
1295
  }
1292
1296
 
1297
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
1293
1298
  /**
1294
1299
  * Classification head postprocessing — softmax + top-k.
1295
1300
  */
@@ -1306,6 +1311,7 @@ export declare interface LetterboxResult {
1306
1311
  */
1307
1312
  export declare function spatialInputSize(shape: DeclaredShape): readonly [number, number] | null;
1308
1313
 
1314
+ /** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */
1309
1315
  /**
1310
1316
  * Per-stage timing for a single `predict()` call.
1311
1317
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tempest-icons.cjs","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"virtual:tempest-icons\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /** Rescan the source tree, keeping the explicitly included slugs. */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* an unreadable file just contributes nothing */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tempest-icons.cjs","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"virtual:tempest-icons\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tempest-icons.js","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"virtual:tempest-icons\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /** Rescan the source tree, keeping the explicitly included slugs. */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* an unreadable file just contributes nothing */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":";;;;;AASA,IAAa,IAAmB,yBAE1B,IAAc,KAAK,KAUnB,IAAgB;CAClB;CACA;CACA;AACJ,GAEM,IAAc,mBACd,oBAAY,IAAI,IAAI;CAAC;CAAgB;CAAQ;CAAS;AAAU,CAAC;AAwBvE,SAAgB,EAAc,GAAc,GAAsC;CAC9E,IAAM,oBAAQ,IAAI,IAAY;CAC9B,KAAK,IAAM,KAAW,GAAe;EACjC,EAAQ,YAAY;EACpB,IAAI;EACJ,QAAQ,IAAQ,EAAQ,KAAK,CAAI,OAAO,OACpC,AAAI,EAAM,IAAI,EAAM,EAAE,KAAG,EAAM,IAAI,EAAM,EAAE;CAEnD;CACA,OAAO,CAAC,GAAG,CAAK;AACpB;AAaA,SAAgB,EACZ,GACA,GACM;CACN,IAAM,IAAS,CAAC,GAAG,IAAI,IAAI,CAAK,CAAC,CAAC,CAAC,KAAK;CACxC,IAAI,CAAC,EAAO,QAAQ,OAAO;CAE3B,IAAM,oBAAc,IAAI,IAAoB;CAC5C,KAAK,IAAM,KAAQ,GAAQ,EAAY,IAAI,GAAM,EAAW,EAAQ,MAAS,CAAI,CAAC;CASlF,OAAO,YAPO,CAAC,GAAG,IAAI,IAAI,EAAY,OAAO,CAAC,CAAC,CAAC,CAC3C,KAAK,CAAC,CACN,KAAK,MAAU,EAAU,CAAI,MAAM,IAAO,IAAO,GAAG,EAAK,MAAM,EAAU,CAAI,GAAI,CAAC,CAClF,KAAK,IAIS,EAAM;;;EAHT,EACX,KAAK,MAAS,QAAQ,EAAK,KAAK,EAAU,EAAY,IAAI,CAAI,CAAW,EAAE,EAAE,CAAC,CAC9E,KAAK,IAIZ,EAAQ;;;AAGV;AAGA,SAAS,EAAW,GAAsB;CACtC,OAAO,EACF,MAAM,GAAG,CAAC,CACV,KAAK,MAAS,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,EAAK,MAAM,CAAC,CAAC,CAAC,CAC3D,KAAK,EAAE;AAChB;AAQA,IAAM,oBAAiB,IAAI,IAAI;CAAC;CAAY;CAAO;CAAa;CAAQ;AAAW,CAAC;AAGpF,SAAS,EAAU,GAAsB;CACrC,OAAO,EAAe,IAAI,CAAI,IAAI,GAAG,EAAK,QAAQ;AACtD;AASA,eAAe,EAAmB,GAAa,GAA8C;CACzF,IAAM,IAAgB,CAAC,GACnB;CACJ,IAAI;EACA,IAAU,MAAM,EAAQ,GAAK,EAAE,eAAe,GAAK,CAAC;CACxD,QAAQ;EACJ,OAAO;CACX;CACA,KAAK,IAAM,KAAS,GAChB,IAAI,EAAM,YAAY,GAAG;EACrB,IAAI,EAAK,IAAI,EAAM,IAAI,KAAK,EAAM,KAAK,WAAW,GAAG,GAAG;EACxD,EAAI,KAAK,GAAI,MAAM,EAAmB,EAAK,GAAK,EAAM,IAAI,GAAG,CAAI,CAAE;CACvE,OAAO,AAAI,EAAM,OAAO,KAAK,EAAY,KAAK,EAAM,IAAI,KACpD,EAAI,KAAK,EAAK,GAAK,EAAM,IAAI,CAAC;CAGtC,OAAO;AACX;AAgCA,SAAgB,EAAa,IAA+B,CAAC,GAAsB;CAC/E,IAAM,EAAE,SAAM,OAAO,aAAU,CAAC,GAAG,gBAAa,GAC1C,IAAQ,IAAI,IAAY,CAAS,GACjC,IAAO,IAAI,IAAY,KAAY,CAAS,GAC5C,IAAQ,IAAI,IAAY,EAAQ,QAAQ,MAAS,EAAM,IAAI,CAAI,CAAC,CAAC,GACnE,IAAO,QAAQ,IAAI,GAGjB,IAAS,YAA2B;EACtC,IAAM,IAAQ,MAAM,EAAmB,EAAQ,GAAM,CAAG,GAAG,CAAI;EAC/D,KAAK,IAAM,KAAQ,GACf,IAAI;GACA,IAAM,IAAO,MAAM,EAAS,GAAM,MAAM;GACxC,KAAK,IAAM,KAAQ,EAAc,GAAM,CAAK,GAAG,EAAM,IAAI,CAAI;EACjE,QAAQ,CAER;CAER;CAEA,OAAO;EACH,MAAM;EACN,SAAS;EAET,eAAe,GAA2B;GACtC,IAAO,EAAO,QAAQ;EAC1B;EAEA,MAAM,aAAa;GACf,MAAM,EAAO;EACjB;EAEA,UAAU,GAAY;GAClB,OAAO,MAAA,0BAA0B,IAAc;EACnD;EAEA,KAAK,GAAY;GACb,OAAO,MAAO,IAAc,EAAiB,CAAC,GAAG,CAAK,GAAG,CAAW,IAAI;EAC5E;EAUA,MAAM,gBAAgB,GAInB;GACC,IAAI,CAAC,EAAY,KAAK,EAAI,IAAI,GAAG;GACjC,IAAM,IAAS,EAAM;GACrB,KAAK,IAAM,KAAQ,EAAc,MAAM,EAAI,KAAK,GAAG,CAAK,GAAG,EAAM,IAAI,CAAI;GACzE,IAAI,EAAM,SAAS,GAAQ;GAC3B,IAAM,IAAM,EAAI,OAAO,YAAY,cAAc,CAAW;GAC5D,IAAI,GAAK,OAAO,CAAC,CAAG;EACxB;CACJ;AACJ"}
1
+ {"version":3,"file":"tempest-icons.js","names":[],"sources":["../../src/vite/tempest-icons.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\n\nimport { iconAliases } from \"@/icons/generated/aliases\";\nimport { iconNames } from \"@/icons/generated/icon-names\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The module id an app imports to get its static registry. */\nexport const TEMPEST_ICONS_ID = \"virtual:tempest-icons\";\n\nconst RESOLVED_ID = `\\0${TEMPEST_ICONS_ID}`;\n\n/**\n * Slugs written as a literal `name` prop or a literal `name:` field.\n *\n * Matching the *attribute* rather than any kebab-case string is what keeps the\n * scan from sweeping in unrelated text — a CSS class, a translation key and a\n * route segment all look like slugs otherwise. Everything matched is still\n * validated against lucide's real slug list before it reaches the module.\n */\nconst NAME_PATTERNS = [\n /\\bname\\s*=\\s*[\"']([a-z0-9-]+)[\"']/g,\n /\\bname\\s*=\\s*\\{\\s*[\"']([a-z0-9-]+)[\"']\\s*\\}/g,\n /\\bname\\s*:\\s*[\"']([a-z0-9-]+)[\"']/g,\n];\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\nexport interface TempestIconsOptions {\n /**\n * Directory to scan, relative to the Vite root. Default: `\"src\"`.\n */\n dir?: string;\n /**\n * Extra slugs to include even when the scan cannot see them — a name built by\n * concatenation, or one that only ever arrives from the API but is worth\n * paying for statically because it is on the first screen.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the icon slugs a source file references literally.\n *\n * @param code - File contents.\n * @param known - The set of real lucide slugs, aliases included.\n * @returns The slugs found, validated against `known`.\n */\nexport function scanIconSlugs(code: string, known: ReadonlySet<string>): string[] {\n const found = new Set<string>();\n for (const pattern of NAME_PATTERNS) {\n pattern.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(code)) !== null) {\n if (known.has(match[1])) found.add(match[1]);\n }\n }\n return [...found];\n}\n\n/**\n * Build the virtual module source for a set of slugs.\n *\n * Aliases resolve to their canonical slug before importing, because only the\n * canonical name is a real lucide export — but the alias key still points at the\n * component, so `<Icon name=\"alert-circle\" />` stays a zero-request lookup.\n *\n * @param slugs - Validated slugs to include.\n * @param aliases - Alias slug → canonical slug.\n * @returns ES module source.\n */\nexport function buildIconsModule(\n slugs: readonly string[],\n aliases: Record<string, string>,\n): string {\n const sorted = [...new Set(slugs)].sort();\n if (!sorted.length) return \"export const staticIcons = {};\\n\";\n\n const componentOf = new Map<string, string>();\n for (const slug of sorted) componentOf.set(slug, pascalCase(aliases[slug] ?? slug));\n\n const named = [...new Set(componentOf.values())]\n .sort()\n .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))\n .join(\", \");\n const entries = sorted\n .map((slug) => ` \"${slug}\": ${localName(componentOf.get(slug) as string)},`)\n .join(\"\\n\");\n return `import { ${named} } from \"lucide-react\";\n\nexport const staticIcons = {\n${entries}\n};\n`;\n}\n\n/** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */\nfunction pascalCase(slug: string): string {\n return slug\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\n/**\n * Export names that would shadow a restricted global if bound directly.\n *\n * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.\n * Binding that at module scope shadows the global, so the import is aliased.\n */\nconst RESERVED_NAMES = new Set([\"Infinity\", \"NaN\", \"undefined\", \"eval\", \"arguments\"]);\n\n/** The local binding to use for a lucide export name. */\nfunction localName(name: string): string {\n return RESERVED_NAMES.has(name) ? `${name}Icon` : name;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Generate a static icon registry from the slugs your source mentions.\n *\n * Removes the reason to reach for `lucide-react`'s `DynamicIcon`: the slugs an app\n * writes as literals become ordinary static imports, so the bundler keeps exactly\n * those icons and the browser makes **no** extra request for them. A slug that only\n * exists at runtime still works — `<Icon>` falls back to fetching one shard per\n * initial letter.\n *\n * The scan reads the source tree directly at `buildStart` rather than harvesting\n * modules as they pass through `transform`: relying on transform order would let\n * the virtual module load before every consumer had been seen, and the registry\n * would come out short on a cold dev start — silently, since the missing slugs\n * would still render via the lazy path.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestIcons()] });\n *\n * @example\n * // src/main.tsx\n * import { IconProvider } from \"tempest-react-sdk/icons\";\n * import { staticIcons } from \"virtual:tempest-icons\";\n *\n * <IconProvider registry={staticIcons}>\n * <App />\n * </IconProvider>\n */\nexport function tempestIcons(options: TempestIconsOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs } = options;\n const known = new Set<string>(iconNames);\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n const slugs = new Set<string>(include.filter((slug) => known.has(slug)));\n let root = process.cwd();\n\n /**\n * Rescan the source tree, keeping the explicitly included slugs.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no slugs and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const files = await collectSourceFiles(resolve(root, dir), skip);\n for (const file of files) {\n try {\n const code = await readFile(file, \"utf8\");\n for (const slug of scanIconSlugs(code, known)) slugs.add(slug);\n } catch {\n /* empty */\n }\n }\n };\n\n return {\n name: \"tempest-icons\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_ICONS_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildIconsModule([...slugs], iconAliases) : null;\n },\n\n /**\n * Pick up a slug added while the dev server is running.\n *\n * A newly written `<Icon name=\"save\" />` would otherwise only render via\n * the lazy path until the next restart. Rescanning the changed file and\n * invalidating the virtual module keeps dev and build agreeing on what the\n * static registry contains.\n */\n async handleHotUpdate(ctx: {\n file: string;\n read: () => Promise<string>;\n server: { moduleGraph: { getModuleById: (id: string) => unknown } };\n }) {\n if (!SOURCE_FILE.test(ctx.file)) return;\n const before = slugs.size;\n for (const slug of scanIconSlugs(await ctx.read(), known)) slugs.add(slug);\n if (slugs.size === before) return;\n const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);\n if (mod) return [mod];\n },\n } as TempestVitePlugin;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tempest-pwa-icons.cjs","names":[],"sources":["../../src/vite/tempest-pwa-icons.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaIcons}. */\nexport interface TempestPwaIconsOptions {\n /** Source image (SVG or large PNG), relative to the project root. Default `public/icon.svg`. */\n source?: string;\n /** Square \"any\"-purpose icon sizes to emit. Default `[192, 512]`. */\n sizes?: number[];\n /** Square \"maskable\" icon sizes to emit (with safe-zone padding). Default `[512]`. */\n maskableSizes?: number[];\n /** Apple touch icon size, or `false` to skip. Default `180`. */\n appleTouchIcon?: number | false;\n /** Output directory for the icon set, under the build root. Default `icons`. */\n outDir?: string;\n /** Opaque background for maskable + apple icons (no transparency allowed). Default `#ffffff`. */\n background?: string;\n /** Maskable safe-zone padding as a fraction of the icon. Default `0.1` (10% each side). */\n maskablePadding?: number;\n /**\n * Generate Apple splash screens (launch images) and inject the matching\n * `<link rel=\"apple-touch-startup-image\">` tags. `true` uses a built-in set\n * of common iPhone/iPad portrait sizes; pass an array to override. Default `false`.\n */\n appleSplash?: boolean | AppleSplashSpec[];\n /** Background color for splash screens. Default: `background`. */\n splashBackground?: string;\n /** Icon size on the splash as a fraction of the shorter side. Default `0.3`. */\n splashIconScale?: number;\n}\n\n/** A single Apple splash target (CSS px + device pixel ratio). */\nexport interface AppleSplashSpec {\n /** CSS width (device-width in the media query). */\n width: number;\n /** CSS height (device-height in the media query). */\n height: number;\n /** Device pixel ratio. */\n ratio: number;\n}\n\n/** Common iPhone/iPad portrait splash sizes (CSS px @ ratio). */\nconst DEFAULT_SPLASH: AppleSplashSpec[] = [\n { width: 375, height: 667, ratio: 2 }, // iPhone SE / 8\n { width: 375, height: 812, ratio: 3 }, // iPhone X / 11 Pro\n { width: 390, height: 844, ratio: 3 }, // iPhone 12 / 13 / 14\n { width: 393, height: 852, ratio: 3 }, // iPhone 14 Pro / 15\n { width: 414, height: 896, ratio: 2 }, // iPhone XR / 11\n { width: 414, height: 896, ratio: 3 }, // iPhone XS Max / 11 Pro Max\n { width: 428, height: 926, ratio: 3 }, // iPhone 13/14 Pro Max\n { width: 430, height: 932, ratio: 3 }, // iPhone 15 Pro Max\n { width: 768, height: 1024, ratio: 2 }, // iPad\n { width: 834, height: 1194, ratio: 2 }, // iPad Pro 11\"\n { width: 1024, height: 1366, ratio: 2 }, // iPad Pro 12.9\"\n];\n\nfunction splashFileName(spec: AppleSplashSpec): string {\n return `splash/apple-splash-${spec.width * spec.ratio}x${spec.height * spec.ratio}.png`;\n}\n\nfunction splashMedia(spec: AppleSplashSpec): string {\n return (\n `(device-width: ${spec.width}px) and (device-height: ${spec.height}px) ` +\n `and (-webkit-device-pixel-ratio: ${spec.ratio}) and (orientation: portrait)`\n );\n}\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nfunction hexToRgb(hex: string): Rgb {\n const value = hex.replace(\"#\", \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : value;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\n/**\n * Build plugin that rasterizes a single source image into a full PWA icon set\n * (regular + maskable + apple-touch-icon), the dependency-free counterpart to\n * `@vite-pwa/assets-generator`. Rendering uses **`sharp`**, imported lazily and\n * treated as optional: if it isn't installed the plugin logs a warning and skips\n * generation (your build still succeeds; the icons just aren't produced).\n *\n * Point your `manifest.webmanifest` icon entries at the emitted files\n * (`/icons/icon-192.png`, `/icons/icon-512.png`, `/icons/maskable-512.png`) and\n * the apple touch icon at `/apple-touch-icon.png`.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaIcons({ source: \"public/icon.svg\" })],\n * });\n */\nexport function tempestPwaIcons(options: TempestPwaIconsOptions = {}): TempestVitePlugin {\n const {\n source = \"public/icon.svg\",\n sizes = [192, 512],\n maskableSizes = [512],\n appleTouchIcon = 180,\n outDir = \"icons\",\n background = \"#ffffff\",\n maskablePadding = 0.1,\n appleSplash = false,\n splashBackground,\n splashIconScale = 0.3,\n } = options;\n\n const splashSpecs: AppleSplashSpec[] = appleSplash\n ? Array.isArray(appleSplash)\n ? appleSplash\n : DEFAULT_SPLASH\n : [];\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-icons\",\n apply: \"build\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n transformIndexHtml() {\n if (!splashSpecs.length) return;\n return splashSpecs.map((spec) => ({\n tag: \"link\",\n attrs: {\n rel: \"apple-touch-startup-image\",\n media: splashMedia(spec),\n href: `/${splashFileName(spec)}`,\n },\n injectTo: \"head\" as const,\n }));\n },\n async generateBundle() {\n let sharp: SharpFactory;\n try {\n // Non-literal specifier so TS doesn't require `sharp`'s types\n // (it is an optional, lazily-loaded dependency).\n const specifier = \"sharp\";\n const mod = (await import(specifier)) as { default?: SharpFactory } & SharpFactory;\n sharp = (mod.default ?? mod) as SharpFactory;\n } catch {\n this.warn(\n \"tempestPwaIcons: `sharp` is not installed — skipping icon generation. \" +\n \"Run `npm i -D sharp` to enable it.\",\n );\n return;\n }\n\n const input = await readFile(resolve(root, source));\n const bg = hexToRgb(background);\n const emit = (fileName: string, data: Buffer): void => {\n this.emitFile({ type: \"asset\", fileName, source: data });\n };\n\n // Regular \"any\" icons — transparent background, full bleed.\n for (const size of sizes) {\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(size, size, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n emit(`${outDir}/icon-${size}.png`, png);\n }\n\n // Maskable icons — content shrunk into the safe zone over a solid bg.\n for (const size of maskableSizes) {\n const content = Math.round(size * (1 - maskablePadding * 2));\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(content, content, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .extend({\n top: Math.round((size - content) / 2),\n bottom: Math.round((size - content) / 2),\n left: Math.round((size - content) / 2),\n right: Math.round((size - content) / 2),\n background: { ...bg, alpha: 1 },\n })\n .resize(size, size)\n .png()\n .toBuffer();\n emit(`${outDir}/maskable-${size}.png`, png);\n }\n\n // Apple touch icon — opaque, no alpha.\n if (appleTouchIcon) {\n const png = await sharp(input, { density: Math.max(appleTouchIcon, 512) })\n .resize(appleTouchIcon, appleTouchIcon, {\n fit: \"contain\",\n background: { ...bg, alpha: 1 },\n })\n .flatten({ background: bg })\n .png()\n .toBuffer();\n emit(\"apple-touch-icon.png\", png);\n }\n\n // Apple splash screens — icon centered on a solid background.\n if (splashSpecs.length) {\n const splashBg = hexToRgb(splashBackground ?? background);\n for (const spec of splashSpecs) {\n const w = spec.width * spec.ratio;\n const h = spec.height * spec.ratio;\n const iconPx = Math.round(Math.min(w, h) * splashIconScale);\n const icon = await sharp(input, { density: Math.max(iconPx, 512) })\n .resize(iconPx, iconPx, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n const png = await sharp({\n create: {\n width: w,\n height: h,\n channels: 4,\n background: { ...splashBg, alpha: 1 },\n },\n })\n .composite([{ input: icon, gravity: \"center\" }])\n .png()\n .toBuffer();\n emit(splashFileName(spec), png);\n }\n }\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n\n/** Options for the sharp `create` (blank canvas) form. */\ninterface SharpCreate {\n create: {\n width: number;\n height: number;\n channels: number;\n background: { r: number; g: number; b: number; alpha: number };\n };\n}\n\n/** The sharp factory function (minimal typing — sharp is an optional dep). */\ntype SharpFactory = (input: Buffer | SharpCreate, opts?: { density?: number }) => SharpInstance;\n\n/** Minimal subset of the sharp chainable API this plugin uses. */\ninterface SharpInstance {\n resize(\n width: number,\n height: number,\n opts?: { fit?: string; background?: { r: number; g: number; b: number; alpha: number } },\n ): SharpInstance;\n extend(opts: {\n top: number;\n bottom: number;\n left: number;\n right: number;\n background: { r: number; g: number; b: number; alpha: number };\n }): SharpInstance;\n flatten(opts: { background: Rgb }): SharpInstance;\n composite(items: { input: Buffer; gravity?: string }[]): SharpInstance;\n png(): SharpInstance;\n toBuffer(): Promise<Buffer>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tempest-pwa-icons.cjs","names":[],"sources":["../../src/vite/tempest-pwa-icons.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the plugin generates every icon size,\n * the maskable variants, the favicon and the Apple touch icons from one source\n * image, and wires them into the manifest it also writes. The sizes and the manifest\n * entries are the same list read twice, so they are produced together.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaIcons}. */\nexport interface TempestPwaIconsOptions {\n /** Source image (SVG or large PNG), relative to the project root. Default `public/icon.svg`. */\n source?: string;\n /** Square \"any\"-purpose icon sizes to emit. Default `[192, 512]`. */\n sizes?: number[];\n /** Square \"maskable\" icon sizes to emit (with safe-zone padding). Default `[512]`. */\n maskableSizes?: number[];\n /** Apple touch icon size, or `false` to skip. Default `180`. */\n appleTouchIcon?: number | false;\n /** Output directory for the icon set, under the build root. Default `icons`. */\n outDir?: string;\n /** Opaque background for maskable + apple icons (no transparency allowed). Default `#ffffff`. */\n background?: string;\n /** Maskable safe-zone padding as a fraction of the icon. Default `0.1` (10% each side). */\n maskablePadding?: number;\n /**\n * Generate Apple splash screens (launch images) and inject the matching\n * `<link rel=\"apple-touch-startup-image\">` tags. `true` uses a built-in set\n * of common iPhone/iPad portrait sizes; pass an array to override. Default `false`.\n */\n appleSplash?: boolean | AppleSplashSpec[];\n /** Background color for splash screens. Default: `background`. */\n splashBackground?: string;\n /** Icon size on the splash as a fraction of the shorter side. Default `0.3`. */\n splashIconScale?: number;\n}\n\n/** A single Apple splash target (CSS px + device pixel ratio). */\nexport interface AppleSplashSpec {\n /** CSS width (device-width in the media query). */\n width: number;\n /** CSS height (device-height in the media query). */\n height: number;\n /** Device pixel ratio. */\n ratio: number;\n}\n\n/** Common iPhone/iPad portrait splash sizes (CSS px @ ratio). */\nconst DEFAULT_SPLASH: AppleSplashSpec[] = [\n { width: 375, height: 667, ratio: 2 }, // iPhone SE / 8\n { width: 375, height: 812, ratio: 3 }, // iPhone X / 11 Pro\n { width: 390, height: 844, ratio: 3 }, // iPhone 12 / 13 / 14\n { width: 393, height: 852, ratio: 3 }, // iPhone 14 Pro / 15\n { width: 414, height: 896, ratio: 2 }, // iPhone XR / 11\n { width: 414, height: 896, ratio: 3 }, // iPhone XS Max / 11 Pro Max\n { width: 428, height: 926, ratio: 3 }, // iPhone 13/14 Pro Max\n { width: 430, height: 932, ratio: 3 }, // iPhone 15 Pro Max\n { width: 768, height: 1024, ratio: 2 }, // iPad\n { width: 834, height: 1194, ratio: 2 }, // iPad Pro 11\"\n { width: 1024, height: 1366, ratio: 2 }, // iPad Pro 12.9\"\n];\n\nfunction splashFileName(spec: AppleSplashSpec): string {\n return `splash/apple-splash-${spec.width * spec.ratio}x${spec.height * spec.ratio}.png`;\n}\n\nfunction splashMedia(spec: AppleSplashSpec): string {\n return (\n `(device-width: ${spec.width}px) and (device-height: ${spec.height}px) ` +\n `and (-webkit-device-pixel-ratio: ${spec.ratio}) and (orientation: portrait)`\n );\n}\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nfunction hexToRgb(hex: string): Rgb {\n const value = hex.replace(\"#\", \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : value;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\n/**\n * Build plugin that rasterizes a single source image into a full PWA icon set\n * (regular + maskable + apple-touch-icon), the dependency-free counterpart to\n * `@vite-pwa/assets-generator`. Rendering uses **`sharp`**, imported lazily and\n * treated as optional: if it isn't installed the plugin logs a warning and skips\n * generation (your build still succeeds; the icons just aren't produced).\n *\n * Point your `manifest.webmanifest` icon entries at the emitted files\n * (`/icons/icon-192.png`, `/icons/icon-512.png`, `/icons/maskable-512.png`) and\n * the apple touch icon at `/apple-touch-icon.png`.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaIcons({ source: \"public/icon.svg\" })],\n * });\n */\nexport function tempestPwaIcons(options: TempestPwaIconsOptions = {}): TempestVitePlugin {\n const {\n source = \"public/icon.svg\",\n sizes = [192, 512],\n maskableSizes = [512],\n appleTouchIcon = 180,\n outDir = \"icons\",\n background = \"#ffffff\",\n maskablePadding = 0.1,\n appleSplash = false,\n splashBackground,\n splashIconScale = 0.3,\n } = options;\n\n const splashSpecs: AppleSplashSpec[] = appleSplash\n ? Array.isArray(appleSplash)\n ? appleSplash\n : DEFAULT_SPLASH\n : [];\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-icons\",\n apply: \"build\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n transformIndexHtml() {\n if (!splashSpecs.length) return;\n return splashSpecs.map((spec) => ({\n tag: \"link\",\n attrs: {\n rel: \"apple-touch-startup-image\",\n media: splashMedia(spec),\n href: `/${splashFileName(spec)}`,\n },\n injectTo: \"head\" as const,\n }));\n },\n async generateBundle() {\n let sharp: SharpFactory;\n try {\n // Non-literal specifier so TS doesn't require `sharp`'s types\n // (it is an optional, lazily-loaded dependency).\n const specifier = \"sharp\";\n const mod = (await import(specifier)) as { default?: SharpFactory } & SharpFactory;\n sharp = (mod.default ?? mod) as SharpFactory;\n } catch {\n this.warn(\n \"tempestPwaIcons: `sharp` is not installed — skipping icon generation. \" +\n \"Run `npm i -D sharp` to enable it.\",\n );\n return;\n }\n\n const input = await readFile(resolve(root, source));\n const bg = hexToRgb(background);\n const emit = (fileName: string, data: Buffer): void => {\n this.emitFile({ type: \"asset\", fileName, source: data });\n };\n\n // Regular \"any\" icons — transparent background, full bleed.\n for (const size of sizes) {\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(size, size, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n emit(`${outDir}/icon-${size}.png`, png);\n }\n\n // Maskable icons — content shrunk into the safe zone over a solid bg.\n for (const size of maskableSizes) {\n const content = Math.round(size * (1 - maskablePadding * 2));\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(content, content, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .extend({\n top: Math.round((size - content) / 2),\n bottom: Math.round((size - content) / 2),\n left: Math.round((size - content) / 2),\n right: Math.round((size - content) / 2),\n background: { ...bg, alpha: 1 },\n })\n .resize(size, size)\n .png()\n .toBuffer();\n emit(`${outDir}/maskable-${size}.png`, png);\n }\n\n // Apple touch icon — opaque, no alpha.\n if (appleTouchIcon) {\n const png = await sharp(input, { density: Math.max(appleTouchIcon, 512) })\n .resize(appleTouchIcon, appleTouchIcon, {\n fit: \"contain\",\n background: { ...bg, alpha: 1 },\n })\n .flatten({ background: bg })\n .png()\n .toBuffer();\n emit(\"apple-touch-icon.png\", png);\n }\n\n // Apple splash screens — icon centered on a solid background.\n if (splashSpecs.length) {\n const splashBg = hexToRgb(splashBackground ?? background);\n for (const spec of splashSpecs) {\n const w = spec.width * spec.ratio;\n const h = spec.height * spec.ratio;\n const iconPx = Math.round(Math.min(w, h) * splashIconScale);\n const icon = await sharp(input, { density: Math.max(iconPx, 512) })\n .resize(iconPx, iconPx, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n const png = await sharp({\n create: {\n width: w,\n height: h,\n channels: 4,\n background: { ...splashBg, alpha: 1 },\n },\n })\n .composite([{ input: icon, gravity: \"center\" }])\n .png()\n .toBuffer();\n emit(splashFileName(spec), png);\n }\n }\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n\n/** Options for the sharp `create` (blank canvas) form. */\ninterface SharpCreate {\n create: {\n width: number;\n height: number;\n channels: number;\n background: { r: number; g: number; b: number; alpha: number };\n };\n}\n\n/** The sharp factory function (minimal typing — sharp is an optional dep). */\ntype SharpFactory = (input: Buffer | SharpCreate, opts?: { density?: number }) => SharpInstance;\n\n/** Minimal subset of the sharp chainable API this plugin uses. */\ninterface SharpInstance {\n resize(\n width: number,\n height: number,\n opts?: { fit?: string; background?: { r: number; g: number; b: number; alpha: number } },\n ): SharpInstance;\n extend(opts: {\n top: number;\n bottom: number;\n left: number;\n right: number;\n background: { r: number; g: number; b: number; alpha: number };\n }): SharpInstance;\n flatten(opts: { background: Rgb }): SharpInstance;\n composite(items: { input: Buffer; gravity?: string }[]): SharpInstance;\n png(): SharpInstance;\n toBuffer(): Promise<Buffer>;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tempest-pwa-icons.js","names":[],"sources":["../../src/vite/tempest-pwa-icons.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaIcons}. */\nexport interface TempestPwaIconsOptions {\n /** Source image (SVG or large PNG), relative to the project root. Default `public/icon.svg`. */\n source?: string;\n /** Square \"any\"-purpose icon sizes to emit. Default `[192, 512]`. */\n sizes?: number[];\n /** Square \"maskable\" icon sizes to emit (with safe-zone padding). Default `[512]`. */\n maskableSizes?: number[];\n /** Apple touch icon size, or `false` to skip. Default `180`. */\n appleTouchIcon?: number | false;\n /** Output directory for the icon set, under the build root. Default `icons`. */\n outDir?: string;\n /** Opaque background for maskable + apple icons (no transparency allowed). Default `#ffffff`. */\n background?: string;\n /** Maskable safe-zone padding as a fraction of the icon. Default `0.1` (10% each side). */\n maskablePadding?: number;\n /**\n * Generate Apple splash screens (launch images) and inject the matching\n * `<link rel=\"apple-touch-startup-image\">` tags. `true` uses a built-in set\n * of common iPhone/iPad portrait sizes; pass an array to override. Default `false`.\n */\n appleSplash?: boolean | AppleSplashSpec[];\n /** Background color for splash screens. Default: `background`. */\n splashBackground?: string;\n /** Icon size on the splash as a fraction of the shorter side. Default `0.3`. */\n splashIconScale?: number;\n}\n\n/** A single Apple splash target (CSS px + device pixel ratio). */\nexport interface AppleSplashSpec {\n /** CSS width (device-width in the media query). */\n width: number;\n /** CSS height (device-height in the media query). */\n height: number;\n /** Device pixel ratio. */\n ratio: number;\n}\n\n/** Common iPhone/iPad portrait splash sizes (CSS px @ ratio). */\nconst DEFAULT_SPLASH: AppleSplashSpec[] = [\n { width: 375, height: 667, ratio: 2 }, // iPhone SE / 8\n { width: 375, height: 812, ratio: 3 }, // iPhone X / 11 Pro\n { width: 390, height: 844, ratio: 3 }, // iPhone 12 / 13 / 14\n { width: 393, height: 852, ratio: 3 }, // iPhone 14 Pro / 15\n { width: 414, height: 896, ratio: 2 }, // iPhone XR / 11\n { width: 414, height: 896, ratio: 3 }, // iPhone XS Max / 11 Pro Max\n { width: 428, height: 926, ratio: 3 }, // iPhone 13/14 Pro Max\n { width: 430, height: 932, ratio: 3 }, // iPhone 15 Pro Max\n { width: 768, height: 1024, ratio: 2 }, // iPad\n { width: 834, height: 1194, ratio: 2 }, // iPad Pro 11\"\n { width: 1024, height: 1366, ratio: 2 }, // iPad Pro 12.9\"\n];\n\nfunction splashFileName(spec: AppleSplashSpec): string {\n return `splash/apple-splash-${spec.width * spec.ratio}x${spec.height * spec.ratio}.png`;\n}\n\nfunction splashMedia(spec: AppleSplashSpec): string {\n return (\n `(device-width: ${spec.width}px) and (device-height: ${spec.height}px) ` +\n `and (-webkit-device-pixel-ratio: ${spec.ratio}) and (orientation: portrait)`\n );\n}\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nfunction hexToRgb(hex: string): Rgb {\n const value = hex.replace(\"#\", \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : value;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\n/**\n * Build plugin that rasterizes a single source image into a full PWA icon set\n * (regular + maskable + apple-touch-icon), the dependency-free counterpart to\n * `@vite-pwa/assets-generator`. Rendering uses **`sharp`**, imported lazily and\n * treated as optional: if it isn't installed the plugin logs a warning and skips\n * generation (your build still succeeds; the icons just aren't produced).\n *\n * Point your `manifest.webmanifest` icon entries at the emitted files\n * (`/icons/icon-192.png`, `/icons/icon-512.png`, `/icons/maskable-512.png`) and\n * the apple touch icon at `/apple-touch-icon.png`.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaIcons({ source: \"public/icon.svg\" })],\n * });\n */\nexport function tempestPwaIcons(options: TempestPwaIconsOptions = {}): TempestVitePlugin {\n const {\n source = \"public/icon.svg\",\n sizes = [192, 512],\n maskableSizes = [512],\n appleTouchIcon = 180,\n outDir = \"icons\",\n background = \"#ffffff\",\n maskablePadding = 0.1,\n appleSplash = false,\n splashBackground,\n splashIconScale = 0.3,\n } = options;\n\n const splashSpecs: AppleSplashSpec[] = appleSplash\n ? Array.isArray(appleSplash)\n ? appleSplash\n : DEFAULT_SPLASH\n : [];\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-icons\",\n apply: \"build\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n transformIndexHtml() {\n if (!splashSpecs.length) return;\n return splashSpecs.map((spec) => ({\n tag: \"link\",\n attrs: {\n rel: \"apple-touch-startup-image\",\n media: splashMedia(spec),\n href: `/${splashFileName(spec)}`,\n },\n injectTo: \"head\" as const,\n }));\n },\n async generateBundle() {\n let sharp: SharpFactory;\n try {\n // Non-literal specifier so TS doesn't require `sharp`'s types\n // (it is an optional, lazily-loaded dependency).\n const specifier = \"sharp\";\n const mod = (await import(specifier)) as { default?: SharpFactory } & SharpFactory;\n sharp = (mod.default ?? mod) as SharpFactory;\n } catch {\n this.warn(\n \"tempestPwaIcons: `sharp` is not installed — skipping icon generation. \" +\n \"Run `npm i -D sharp` to enable it.\",\n );\n return;\n }\n\n const input = await readFile(resolve(root, source));\n const bg = hexToRgb(background);\n const emit = (fileName: string, data: Buffer): void => {\n this.emitFile({ type: \"asset\", fileName, source: data });\n };\n\n // Regular \"any\" icons — transparent background, full bleed.\n for (const size of sizes) {\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(size, size, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n emit(`${outDir}/icon-${size}.png`, png);\n }\n\n // Maskable icons — content shrunk into the safe zone over a solid bg.\n for (const size of maskableSizes) {\n const content = Math.round(size * (1 - maskablePadding * 2));\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(content, content, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .extend({\n top: Math.round((size - content) / 2),\n bottom: Math.round((size - content) / 2),\n left: Math.round((size - content) / 2),\n right: Math.round((size - content) / 2),\n background: { ...bg, alpha: 1 },\n })\n .resize(size, size)\n .png()\n .toBuffer();\n emit(`${outDir}/maskable-${size}.png`, png);\n }\n\n // Apple touch icon — opaque, no alpha.\n if (appleTouchIcon) {\n const png = await sharp(input, { density: Math.max(appleTouchIcon, 512) })\n .resize(appleTouchIcon, appleTouchIcon, {\n fit: \"contain\",\n background: { ...bg, alpha: 1 },\n })\n .flatten({ background: bg })\n .png()\n .toBuffer();\n emit(\"apple-touch-icon.png\", png);\n }\n\n // Apple splash screens — icon centered on a solid background.\n if (splashSpecs.length) {\n const splashBg = hexToRgb(splashBackground ?? background);\n for (const spec of splashSpecs) {\n const w = spec.width * spec.ratio;\n const h = spec.height * spec.ratio;\n const iconPx = Math.round(Math.min(w, h) * splashIconScale);\n const icon = await sharp(input, { density: Math.max(iconPx, 512) })\n .resize(iconPx, iconPx, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n const png = await sharp({\n create: {\n width: w,\n height: h,\n channels: 4,\n background: { ...splashBg, alpha: 1 },\n },\n })\n .composite([{ input: icon, gravity: \"center\" }])\n .png()\n .toBuffer();\n emit(splashFileName(spec), png);\n }\n }\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n\n/** Options for the sharp `create` (blank canvas) form. */\ninterface SharpCreate {\n create: {\n width: number;\n height: number;\n channels: number;\n background: { r: number; g: number; b: number; alpha: number };\n };\n}\n\n/** The sharp factory function (minimal typing — sharp is an optional dep). */\ntype SharpFactory = (input: Buffer | SharpCreate, opts?: { density?: number }) => SharpInstance;\n\n/** Minimal subset of the sharp chainable API this plugin uses. */\ninterface SharpInstance {\n resize(\n width: number,\n height: number,\n opts?: { fit?: string; background?: { r: number; g: number; b: number; alpha: number } },\n ): SharpInstance;\n extend(opts: {\n top: number;\n bottom: number;\n left: number;\n right: number;\n background: { r: number; g: number; b: number; alpha: number };\n }): SharpInstance;\n flatten(opts: { background: Rgb }): SharpInstance;\n composite(items: { input: Buffer; gravity?: string }[]): SharpInstance;\n png(): SharpInstance;\n toBuffer(): Promise<Buffer>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tempest-pwa-icons.js","names":[],"sources":["../../src/vite/tempest-pwa-icons.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the plugin generates every icon size,\n * the maskable variants, the favicon and the Apple touch icons from one source\n * image, and wires them into the manifest it also writes. The sizes and the manifest\n * entries are the same list read twice, so they are produced together.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaIcons}. */\nexport interface TempestPwaIconsOptions {\n /** Source image (SVG or large PNG), relative to the project root. Default `public/icon.svg`. */\n source?: string;\n /** Square \"any\"-purpose icon sizes to emit. Default `[192, 512]`. */\n sizes?: number[];\n /** Square \"maskable\" icon sizes to emit (with safe-zone padding). Default `[512]`. */\n maskableSizes?: number[];\n /** Apple touch icon size, or `false` to skip. Default `180`. */\n appleTouchIcon?: number | false;\n /** Output directory for the icon set, under the build root. Default `icons`. */\n outDir?: string;\n /** Opaque background for maskable + apple icons (no transparency allowed). Default `#ffffff`. */\n background?: string;\n /** Maskable safe-zone padding as a fraction of the icon. Default `0.1` (10% each side). */\n maskablePadding?: number;\n /**\n * Generate Apple splash screens (launch images) and inject the matching\n * `<link rel=\"apple-touch-startup-image\">` tags. `true` uses a built-in set\n * of common iPhone/iPad portrait sizes; pass an array to override. Default `false`.\n */\n appleSplash?: boolean | AppleSplashSpec[];\n /** Background color for splash screens. Default: `background`. */\n splashBackground?: string;\n /** Icon size on the splash as a fraction of the shorter side. Default `0.3`. */\n splashIconScale?: number;\n}\n\n/** A single Apple splash target (CSS px + device pixel ratio). */\nexport interface AppleSplashSpec {\n /** CSS width (device-width in the media query). */\n width: number;\n /** CSS height (device-height in the media query). */\n height: number;\n /** Device pixel ratio. */\n ratio: number;\n}\n\n/** Common iPhone/iPad portrait splash sizes (CSS px @ ratio). */\nconst DEFAULT_SPLASH: AppleSplashSpec[] = [\n { width: 375, height: 667, ratio: 2 }, // iPhone SE / 8\n { width: 375, height: 812, ratio: 3 }, // iPhone X / 11 Pro\n { width: 390, height: 844, ratio: 3 }, // iPhone 12 / 13 / 14\n { width: 393, height: 852, ratio: 3 }, // iPhone 14 Pro / 15\n { width: 414, height: 896, ratio: 2 }, // iPhone XR / 11\n { width: 414, height: 896, ratio: 3 }, // iPhone XS Max / 11 Pro Max\n { width: 428, height: 926, ratio: 3 }, // iPhone 13/14 Pro Max\n { width: 430, height: 932, ratio: 3 }, // iPhone 15 Pro Max\n { width: 768, height: 1024, ratio: 2 }, // iPad\n { width: 834, height: 1194, ratio: 2 }, // iPad Pro 11\"\n { width: 1024, height: 1366, ratio: 2 }, // iPad Pro 12.9\"\n];\n\nfunction splashFileName(spec: AppleSplashSpec): string {\n return `splash/apple-splash-${spec.width * spec.ratio}x${spec.height * spec.ratio}.png`;\n}\n\nfunction splashMedia(spec: AppleSplashSpec): string {\n return (\n `(device-width: ${spec.width}px) and (device-height: ${spec.height}px) ` +\n `and (-webkit-device-pixel-ratio: ${spec.ratio}) and (orientation: portrait)`\n );\n}\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nfunction hexToRgb(hex: string): Rgb {\n const value = hex.replace(\"#\", \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : value;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\n/**\n * Build plugin that rasterizes a single source image into a full PWA icon set\n * (regular + maskable + apple-touch-icon), the dependency-free counterpart to\n * `@vite-pwa/assets-generator`. Rendering uses **`sharp`**, imported lazily and\n * treated as optional: if it isn't installed the plugin logs a warning and skips\n * generation (your build still succeeds; the icons just aren't produced).\n *\n * Point your `manifest.webmanifest` icon entries at the emitted files\n * (`/icons/icon-192.png`, `/icons/icon-512.png`, `/icons/maskable-512.png`) and\n * the apple touch icon at `/apple-touch-icon.png`.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaIcons({ source: \"public/icon.svg\" })],\n * });\n */\nexport function tempestPwaIcons(options: TempestPwaIconsOptions = {}): TempestVitePlugin {\n const {\n source = \"public/icon.svg\",\n sizes = [192, 512],\n maskableSizes = [512],\n appleTouchIcon = 180,\n outDir = \"icons\",\n background = \"#ffffff\",\n maskablePadding = 0.1,\n appleSplash = false,\n splashBackground,\n splashIconScale = 0.3,\n } = options;\n\n const splashSpecs: AppleSplashSpec[] = appleSplash\n ? Array.isArray(appleSplash)\n ? appleSplash\n : DEFAULT_SPLASH\n : [];\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-icons\",\n apply: \"build\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n transformIndexHtml() {\n if (!splashSpecs.length) return;\n return splashSpecs.map((spec) => ({\n tag: \"link\",\n attrs: {\n rel: \"apple-touch-startup-image\",\n media: splashMedia(spec),\n href: `/${splashFileName(spec)}`,\n },\n injectTo: \"head\" as const,\n }));\n },\n async generateBundle() {\n let sharp: SharpFactory;\n try {\n // Non-literal specifier so TS doesn't require `sharp`'s types\n // (it is an optional, lazily-loaded dependency).\n const specifier = \"sharp\";\n const mod = (await import(specifier)) as { default?: SharpFactory } & SharpFactory;\n sharp = (mod.default ?? mod) as SharpFactory;\n } catch {\n this.warn(\n \"tempestPwaIcons: `sharp` is not installed — skipping icon generation. \" +\n \"Run `npm i -D sharp` to enable it.\",\n );\n return;\n }\n\n const input = await readFile(resolve(root, source));\n const bg = hexToRgb(background);\n const emit = (fileName: string, data: Buffer): void => {\n this.emitFile({ type: \"asset\", fileName, source: data });\n };\n\n // Regular \"any\" icons — transparent background, full bleed.\n for (const size of sizes) {\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(size, size, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n emit(`${outDir}/icon-${size}.png`, png);\n }\n\n // Maskable icons — content shrunk into the safe zone over a solid bg.\n for (const size of maskableSizes) {\n const content = Math.round(size * (1 - maskablePadding * 2));\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(content, content, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .extend({\n top: Math.round((size - content) / 2),\n bottom: Math.round((size - content) / 2),\n left: Math.round((size - content) / 2),\n right: Math.round((size - content) / 2),\n background: { ...bg, alpha: 1 },\n })\n .resize(size, size)\n .png()\n .toBuffer();\n emit(`${outDir}/maskable-${size}.png`, png);\n }\n\n // Apple touch icon — opaque, no alpha.\n if (appleTouchIcon) {\n const png = await sharp(input, { density: Math.max(appleTouchIcon, 512) })\n .resize(appleTouchIcon, appleTouchIcon, {\n fit: \"contain\",\n background: { ...bg, alpha: 1 },\n })\n .flatten({ background: bg })\n .png()\n .toBuffer();\n emit(\"apple-touch-icon.png\", png);\n }\n\n // Apple splash screens — icon centered on a solid background.\n if (splashSpecs.length) {\n const splashBg = hexToRgb(splashBackground ?? background);\n for (const spec of splashSpecs) {\n const w = spec.width * spec.ratio;\n const h = spec.height * spec.ratio;\n const iconPx = Math.round(Math.min(w, h) * splashIconScale);\n const icon = await sharp(input, { density: Math.max(iconPx, 512) })\n .resize(iconPx, iconPx, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n const png = await sharp({\n create: {\n width: w,\n height: h,\n channels: 4,\n background: { ...splashBg, alpha: 1 },\n },\n })\n .composite([{ input: icon, gravity: \"center\" }])\n .png()\n .toBuffer();\n emit(splashFileName(spec), png);\n }\n }\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n\n/** Options for the sharp `create` (blank canvas) form. */\ninterface SharpCreate {\n create: {\n width: number;\n height: number;\n channels: number;\n background: { r: number; g: number; b: number; alpha: number };\n };\n}\n\n/** The sharp factory function (minimal typing — sharp is an optional dep). */\ntype SharpFactory = (input: Buffer | SharpCreate, opts?: { density?: number }) => SharpInstance;\n\n/** Minimal subset of the sharp chainable API this plugin uses. */\ninterface SharpInstance {\n resize(\n width: number,\n height: number,\n opts?: { fit?: string; background?: { r: number; g: number; b: number; alpha: number } },\n ): SharpInstance;\n extend(opts: {\n top: number;\n bottom: number;\n left: number;\n right: number;\n background: { r: number; g: number; b: number; alpha: number };\n }): SharpInstance;\n flatten(opts: { background: Rgb }): SharpInstance;\n composite(items: { input: Buffer; gravity?: string }[]): SharpInstance;\n png(): SharpInstance;\n toBuffer(): Promise<Buffer>;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-web-socket.cjs","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["export type WebSocketStatus = \"idle\" | \"connecting\" | \"open\" | \"closing\" | \"closed\" | \"error\";\n\nexport interface WebSocketMessage<T> {\n /** Parsed payload — JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** The original `MessageEvent`. */\n raw: MessageEvent;\n}\n\nexport interface CreateWebSocketOptions<T> {\n /** Subprotocol(s) forwarded to the `WebSocket` constructor. */\n protocols?: string | string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable. */\n maxRetries?: number;\n /** Initial backoff (ms). Doubles each attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff (ms). Default: 30000. */\n maxBackoff?: number;\n /**\n * Ping interval (ms). When set, the client sends `pingPayload` periodically\n * to keep the socket alive. Default: 0 (disabled).\n *\n * Leave it off against a `tempest-fastapi-sdk` server: that server pings on\n * its own and answers a client-sent `{\"type\":\"ping\"}` with nothing, while a\n * strict handler rejects the unknown frame. What it needs from the client\n * is the `pong` reply, which `respondToPing` sends for you.\n */\n pingInterval?: number;\n /** Payload sent on each ping. Default: `JSON.stringify({ type: \"ping\" })`. */\n pingPayload?: string | Blob | BufferSource;\n /**\n * Reply to a server `{\"type\":\"ping\"}` with `pongPayload`. Default: true.\n *\n * `tempest-fastapi-sdk` closes a socket with code `4408` when no `pong`\n * arrives within `WS_HEARTBEAT_TIMEOUT_SECONDS`, so a client that stays\n * silent is dropped once per timeout. The ping is still forwarded to\n * `onMessage` — the reply is sent before your handler runs.\n */\n respondToPing?: boolean;\n /** Payload sent in reply to a server ping. Default: `JSON.stringify({ type: \"pong\" })`. */\n pongPayload?: string | Blob | BufferSource;\n /**\n * Buffer payloads sent while the socket is not open and flush them on the\n * next `open`. Default: false — `send()` returns false and drops.\n *\n * Without it, an action fired during reconnect backoff vanishes and the UI\n * cannot tell \"never sent\" from \"sent and ignored\".\n */\n queueWhileClosed?: boolean;\n /** Cap on buffered payloads when `queueWhileClosed` is on. Default: 100. */\n maxQueuedMessages?: number;\n /** Parse incoming frames. Default: JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n onOpen?: (event: Event) => void;\n onMessage?: (message: WebSocketMessage<T>) => void;\n onClose?: (event: CloseEvent) => void;\n onError?: (event: Event) => void;\n onStatusChange?: (status: WebSocketStatus) => void;\n}\n\nexport interface WebSocketController {\n /** Send a payload over the current connection. No-op when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Close the connection and stop reconnecting. */\n close: (code?: number, reason?: string) => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\nfunction defaultParser<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * Open a WebSocket with automatic exponential-backoff reconnect, optional\n * heartbeat pings, and typed JSON parsing.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, and `status`.\n */\nexport function createWebSocket<T = unknown>(\n url: string,\n options: CreateWebSocketOptions<T> = {},\n): WebSocketController {\n const {\n protocols,\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n pingInterval = 0,\n pingPayload = JSON.stringify({ type: \"ping\" }),\n respondToPing = true,\n pongPayload = JSON.stringify({ type: \"pong\" }),\n queueWhileClosed = false,\n maxQueuedMessages = 100,\n parser = defaultParser<T>,\n onOpen,\n onMessage,\n onClose,\n onError,\n onStatusChange,\n } = options;\n\n let socket: WebSocket | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\n const outbox: Array<string | Blob | BufferSource> = [];\n\n /** True for a decoded frame that is the server's heartbeat ping. */\n function isServerPing(data: unknown): boolean {\n return (\n typeof data === \"object\" &&\n data !== null &&\n (data as { type?: unknown }).type === \"ping\"\n );\n }\n\n /** Send everything buffered while the socket was down, oldest first. */\n function flushOutbox(ws: WebSocket): void {\n while (outbox.length > 0 && ws.readyState === WebSocket.OPEN) {\n ws.send(outbox.shift()!);\n }\n }\n\n function setStatus(next: WebSocketStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function clearPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n function startPing(): void {\n if (!pingInterval || pingInterval <= 0) return;\n clearPing();\n pingTimer = setInterval(() => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(pingPayload);\n }\n }, pingInterval);\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function connect(): void {\n if (closed) return;\n if (socket) {\n const previous = socket;\n previous.onmessage = null;\n previous.onclose = null;\n previous.onerror = null;\n if (previous.readyState !== WebSocket.CONNECTING) previous.onopen = null;\n closeSocket(previous);\n }\n setStatus(\"connecting\");\n\n const ws = new WebSocket(url, protocols);\n socket = ws;\n\n ws.onopen = (event) => {\n retries = 0;\n setStatus(\"open\");\n startPing();\n flushOutbox(ws);\n onOpen?.(event);\n };\n\n ws.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n const data = parser(raw);\n if (respondToPing && isServerPing(data) && ws.readyState === WebSocket.OPEN) {\n ws.send(pongPayload);\n }\n onMessage?.({ data, raw: event });\n };\n\n ws.onerror = (event) => {\n onError?.(event);\n };\n\n ws.onclose = (event) => {\n clearPing();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (!closed && !event.wasClean) {\n scheduleReconnect();\n }\n };\n }\n\n function send(payload: string | Blob | BufferSource): boolean {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(payload);\n return true;\n }\n if (!queueWhileClosed || closed) return false;\n if (outbox.length >= maxQueuedMessages) outbox.shift();\n outbox.push(payload);\n return true;\n }\n\n function close(code?: number, reason?: string): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearPing();\n retries = 0;\n outbox.length = 0;\n if (socket) {\n setStatus(\"closing\");\n closeSocket(socket, code, reason);\n socket = null;\n }\n setStatus(\"closed\");\n }\n\n /**\n * Close a socket without the \"closed before the connection is established\"\n * console warning.\n *\n * A socket still in `CONNECTING` cannot be closed cleanly — the browser\n * logs that warning on every attempt. React's StrictMode mounts, unmounts\n * and remounts each component in development, so the first socket is\n * always torn down mid-handshake and the message shows up in every dev\n * session of every app using the hook. Deferring the close to `onopen`\n * costs one round trip and keeps the console usable.\n */\n function closeSocket(ws: WebSocket, code?: number, reason?: string): void {\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.onopen = () => ws.close(code, reason);\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n return;\n }\n ws.close(code, reason);\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-web-socket.cjs","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — reconnect with backoff, heartbeat,\n * the send queue that survives a disconnect and the listener set that must be re-\n * attached to each new socket — one connection's lifetime, one closure. The queue\n * and the reconnect timer are the same decision seen twice.\n */\nexport type WebSocketStatus = \"idle\" | \"connecting\" | \"open\" | \"closing\" | \"closed\" | \"error\";\n\nexport interface WebSocketMessage<T> {\n /** Parsed payload — JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** The original `MessageEvent`. */\n raw: MessageEvent;\n}\n\nexport interface CreateWebSocketOptions<T> {\n /** Subprotocol(s) forwarded to the `WebSocket` constructor. */\n protocols?: string | string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable. */\n maxRetries?: number;\n /** Initial backoff (ms). Doubles each attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff (ms). Default: 30000. */\n maxBackoff?: number;\n /**\n * Ping interval (ms). When set, the client sends `pingPayload` periodically\n * to keep the socket alive. Default: 0 (disabled).\n *\n * Leave it off against a `tempest-fastapi-sdk` server: that server pings on\n * its own and answers a client-sent `{\"type\":\"ping\"}` with nothing, while a\n * strict handler rejects the unknown frame. What it needs from the client\n * is the `pong` reply, which `respondToPing` sends for you.\n */\n pingInterval?: number;\n /** Payload sent on each ping. Default: `JSON.stringify({ type: \"ping\" })`. */\n pingPayload?: string | Blob | BufferSource;\n /**\n * Reply to a server `{\"type\":\"ping\"}` with `pongPayload`. Default: true.\n *\n * `tempest-fastapi-sdk` closes a socket with code `4408` when no `pong`\n * arrives within `WS_HEARTBEAT_TIMEOUT_SECONDS`, so a client that stays\n * silent is dropped once per timeout. The ping is still forwarded to\n * `onMessage` — the reply is sent before your handler runs.\n */\n respondToPing?: boolean;\n /** Payload sent in reply to a server ping. Default: `JSON.stringify({ type: \"pong\" })`. */\n pongPayload?: string | Blob | BufferSource;\n /**\n * Buffer payloads sent while the socket is not open and flush them on the\n * next `open`. Default: false — `send()` returns false and drops.\n *\n * Without it, an action fired during reconnect backoff vanishes and the UI\n * cannot tell \"never sent\" from \"sent and ignored\".\n */\n queueWhileClosed?: boolean;\n /** Cap on buffered payloads when `queueWhileClosed` is on. Default: 100. */\n maxQueuedMessages?: number;\n /** Parse incoming frames. Default: JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n onOpen?: (event: Event) => void;\n onMessage?: (message: WebSocketMessage<T>) => void;\n onClose?: (event: CloseEvent) => void;\n onError?: (event: Event) => void;\n onStatusChange?: (status: WebSocketStatus) => void;\n}\n\nexport interface WebSocketController {\n /** Send a payload over the current connection. No-op when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Close the connection and stop reconnecting. */\n close: (code?: number, reason?: string) => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\nfunction defaultParser<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * Open a WebSocket with automatic exponential-backoff reconnect, optional\n * heartbeat pings, and typed JSON parsing.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, and `status`.\n */\nexport function createWebSocket<T = unknown>(\n url: string,\n options: CreateWebSocketOptions<T> = {},\n): WebSocketController {\n const {\n protocols,\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n pingInterval = 0,\n pingPayload = JSON.stringify({ type: \"ping\" }),\n respondToPing = true,\n pongPayload = JSON.stringify({ type: \"pong\" }),\n queueWhileClosed = false,\n maxQueuedMessages = 100,\n parser = defaultParser<T>,\n onOpen,\n onMessage,\n onClose,\n onError,\n onStatusChange,\n } = options;\n\n let socket: WebSocket | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\n const outbox: Array<string | Blob | BufferSource> = [];\n\n /** True for a decoded frame that is the server's heartbeat ping. */\n function isServerPing(data: unknown): boolean {\n return (\n typeof data === \"object\" &&\n data !== null &&\n (data as { type?: unknown }).type === \"ping\"\n );\n }\n\n /** Send everything buffered while the socket was down, oldest first. */\n function flushOutbox(ws: WebSocket): void {\n while (outbox.length > 0 && ws.readyState === WebSocket.OPEN) {\n ws.send(outbox.shift()!);\n }\n }\n\n function setStatus(next: WebSocketStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function clearPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n function startPing(): void {\n if (!pingInterval || pingInterval <= 0) return;\n clearPing();\n pingTimer = setInterval(() => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(pingPayload);\n }\n }, pingInterval);\n }\n\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function connect(): void {\n if (closed) return;\n if (socket) {\n const previous = socket;\n previous.onmessage = null;\n previous.onclose = null;\n previous.onerror = null;\n if (previous.readyState !== WebSocket.CONNECTING) previous.onopen = null;\n closeSocket(previous);\n }\n setStatus(\"connecting\");\n\n const ws = new WebSocket(url, protocols);\n socket = ws;\n\n ws.onopen = (event) => {\n retries = 0;\n setStatus(\"open\");\n startPing();\n flushOutbox(ws);\n onOpen?.(event);\n };\n\n ws.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n const data = parser(raw);\n if (respondToPing && isServerPing(data) && ws.readyState === WebSocket.OPEN) {\n ws.send(pongPayload);\n }\n onMessage?.({ data, raw: event });\n };\n\n ws.onerror = (event) => {\n onError?.(event);\n };\n\n ws.onclose = (event) => {\n clearPing();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (!closed && !event.wasClean) {\n scheduleReconnect();\n }\n };\n }\n\n function send(payload: string | Blob | BufferSource): boolean {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(payload);\n return true;\n }\n if (!queueWhileClosed || closed) return false;\n if (outbox.length >= maxQueuedMessages) outbox.shift();\n outbox.push(payload);\n return true;\n }\n\n function close(code?: number, reason?: string): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearPing();\n retries = 0;\n outbox.length = 0;\n if (socket) {\n setStatus(\"closing\");\n closeSocket(socket, code, reason);\n socket = null;\n }\n setStatus(\"closed\");\n }\n\n /**\n * Close a socket without the \"closed before the connection is established\"\n * console warning.\n *\n * A socket still in `CONNECTING` cannot be closed cleanly — the browser\n * logs that warning on every attempt. React's StrictMode mounts, unmounts\n * and remounts each component in development, so the first socket is\n * always torn down mid-handshake and the message shows up in every dev\n * session of every app using the hook. Deferring the close to `onopen`\n * costs one round trip and keeps the console usable.\n */\n function closeSocket(ws: WebSocket, code?: number, reason?: string): void {\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.onopen = () => ws.close(code, reason);\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n return;\n }\n ws.close(code, reason);\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}