tempest-react-sdk 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +40 -10
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/component/generate.test.mjs +13 -1
  5. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  6. package/bin/lib/doctor/vitest.mjs +120 -0
  7. package/bin/lib/doctor/vitest.test.mjs +139 -0
  8. package/bin/lib/fs/contain.mjs +7 -0
  9. package/bin/lib/openapi/escape.mjs +68 -4
  10. package/bin/lib/openapi/generate.hostile.test.mjs +150 -1
  11. package/bin/lib/openapi/generate.mjs +78 -13
  12. package/bin/lib/openapi/generate.test.mjs +13 -1
  13. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  14. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  15. package/bin/tempest-gen.test.mjs +100 -0
  16. package/bin/tempest.mjs +24 -8
  17. package/dist/access/use-can.cjs +1 -1
  18. package/dist/access/use-can.cjs.map +1 -1
  19. package/dist/access/use-can.js +25 -28
  20. package/dist/access/use-can.js.map +1 -1
  21. package/dist/auth/create-tempest-auth.cjs +1 -1
  22. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  23. package/dist/auth/create-tempest-auth.js +234 -122
  24. package/dist/auth/create-tempest-auth.js.map +1 -1
  25. package/dist/auth/session-identity.cjs +2 -0
  26. package/dist/auth/session-identity.cjs.map +1 -0
  27. package/dist/auth/session-identity.js +12 -0
  28. package/dist/auth/session-identity.js.map +1 -0
  29. package/dist/br/boleto.cjs +1 -1
  30. package/dist/br/boleto.cjs.map +1 -1
  31. package/dist/br/boleto.js +2 -1
  32. package/dist/br/boleto.js.map +1 -1
  33. package/dist/br.d.ts +22 -3
  34. package/dist/components/AuthForm/AuthFieldControl.cjs +2 -0
  35. package/dist/components/AuthForm/AuthFieldControl.cjs.map +1 -0
  36. package/dist/components/AuthForm/AuthFieldControl.js +85 -0
  37. package/dist/components/AuthForm/AuthFieldControl.js.map +1 -0
  38. package/dist/components/AuthForm/AuthForm.cjs +2 -0
  39. package/dist/components/AuthForm/AuthForm.cjs.map +1 -0
  40. package/dist/components/AuthForm/AuthForm.js +83 -0
  41. package/dist/components/AuthForm/AuthForm.js.map +1 -0
  42. package/dist/components/AuthForm/AuthForm.module.cjs +3 -0
  43. package/dist/components/AuthForm/AuthForm.module.cjs.map +1 -0
  44. package/dist/components/AuthForm/AuthForm.module.js +14 -0
  45. package/dist/components/AuthForm/AuthForm.module.js.map +1 -0
  46. package/dist/components/AuthForm/AuthLinks.cjs +2 -0
  47. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -0
  48. package/dist/components/AuthForm/AuthLinks.js +32 -0
  49. package/dist/components/AuthForm/AuthLinks.js.map +1 -0
  50. package/dist/components/AuthForm/ForgotPasswordForm.cjs +2 -0
  51. package/dist/components/AuthForm/ForgotPasswordForm.cjs.map +1 -0
  52. package/dist/components/AuthForm/ForgotPasswordForm.js +32 -0
  53. package/dist/components/AuthForm/ForgotPasswordForm.js.map +1 -0
  54. package/dist/components/AuthForm/LoginForm.cjs +2 -0
  55. package/dist/components/AuthForm/LoginForm.cjs.map +1 -0
  56. package/dist/components/AuthForm/LoginForm.js +39 -0
  57. package/dist/components/AuthForm/LoginForm.js.map +1 -0
  58. package/dist/components/AuthForm/ResetPasswordForm.cjs +2 -0
  59. package/dist/components/AuthForm/ResetPasswordForm.cjs.map +1 -0
  60. package/dist/components/AuthForm/ResetPasswordForm.js +39 -0
  61. package/dist/components/AuthForm/ResetPasswordForm.js.map +1 -0
  62. package/dist/components/AuthForm/SignUpForm.cjs +2 -0
  63. package/dist/components/AuthForm/SignUpForm.cjs.map +1 -0
  64. package/dist/components/AuthForm/SignUpForm.js +32 -0
  65. package/dist/components/AuthForm/SignUpForm.js.map +1 -0
  66. package/dist/components/AuthForm/auth-defaults.cjs +2 -0
  67. package/dist/components/AuthForm/auth-defaults.cjs.map +1 -0
  68. package/dist/components/AuthForm/auth-defaults.js +54 -0
  69. package/dist/components/AuthForm/auth-defaults.js.map +1 -0
  70. package/dist/components/AuthForm/auth-schemas.cjs +2 -0
  71. package/dist/components/AuthForm/auth-schemas.cjs.map +1 -0
  72. package/dist/components/AuthForm/auth-schemas.js +26 -0
  73. package/dist/components/AuthForm/auth-schemas.js.map +1 -0
  74. package/dist/components/AuthPage/AuthPage.cjs +2 -0
  75. package/dist/components/AuthPage/AuthPage.cjs.map +1 -0
  76. package/dist/components/AuthPage/AuthPage.js +97 -0
  77. package/dist/components/AuthPage/AuthPage.js.map +1 -0
  78. package/dist/components/AuthPage/AuthPage.module.cjs +3 -0
  79. package/dist/components/AuthPage/AuthPage.module.cjs.map +1 -0
  80. package/dist/components/AuthPage/AuthPage.module.js +24 -0
  81. package/dist/components/AuthPage/AuthPage.module.js.map +1 -0
  82. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  83. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  84. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  85. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  86. package/dist/components/Button/Button.cjs +1 -1
  87. package/dist/components/Button/Button.cjs.map +1 -1
  88. package/dist/components/Button/Button.js +41 -40
  89. package/dist/components/Button/Button.js.map +1 -1
  90. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  91. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  92. package/dist/components/Chat/ChatAttachments.js +38 -38
  93. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  94. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  95. package/dist/components/FilterBar/filter-apply.js +7 -7
  96. package/dist/components/Form/Form.cjs +1 -1
  97. package/dist/components/Form/Form.cjs.map +1 -1
  98. package/dist/components/Form/Form.js +30 -32
  99. package/dist/components/Form/Form.js.map +1 -1
  100. package/dist/components/Layout/Layout.cjs +1 -1
  101. package/dist/components/Layout/Layout.cjs.map +1 -1
  102. package/dist/components/Layout/Layout.js +25 -24
  103. package/dist/components/Layout/Layout.js.map +1 -1
  104. package/dist/components/Layout/gap.cjs +2 -0
  105. package/dist/components/Layout/gap.cjs.map +1 -0
  106. package/dist/components/Layout/gap.js +15 -0
  107. package/dist/components/Layout/gap.js.map +1 -0
  108. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  109. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  110. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  111. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  112. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  113. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  114. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  115. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  116. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  117. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  118. package/dist/data/create-data-provider.cjs +1 -1
  119. package/dist/data/create-data-provider.cjs.map +1 -1
  120. package/dist/data/create-data-provider.js +1 -2
  121. package/dist/data/create-data-provider.js.map +1 -1
  122. package/dist/forms/index.js +7 -7
  123. package/dist/forms/masked-inputs.cjs +1 -1
  124. package/dist/forms/masked-inputs.cjs.map +1 -1
  125. package/dist/forms/masked-inputs.js +15 -15
  126. package/dist/forms/validate-form.cjs +1 -1
  127. package/dist/forms/validate-form.cjs.map +1 -1
  128. package/dist/forms/validate-form.js +6 -1
  129. package/dist/forms/validate-form.js.map +1 -1
  130. package/dist/forms/zod-resolver.cjs +1 -1
  131. package/dist/forms/zod-resolver.cjs.map +1 -1
  132. package/dist/forms/zod-resolver.js +8 -3
  133. package/dist/forms/zod-resolver.js.map +1 -1
  134. package/dist/http/api-client.cjs +1 -1
  135. package/dist/http/api-client.cjs.map +1 -1
  136. package/dist/http/api-client.js +137 -116
  137. package/dist/http/api-client.js.map +1 -1
  138. package/dist/http/credential-scope.cjs +1 -1
  139. package/dist/http/credential-scope.cjs.map +1 -1
  140. package/dist/http/credential-scope.js +33 -9
  141. package/dist/http/credential-scope.js.map +1 -1
  142. package/dist/http/presigned-post.cjs.map +1 -1
  143. package/dist/http/presigned-post.js.map +1 -1
  144. package/dist/http/resumable-upload.cjs +1 -1
  145. package/dist/http/resumable-upload.cjs.map +1 -1
  146. package/dist/http/resumable-upload.js +140 -134
  147. package/dist/http/resumable-upload.js.map +1 -1
  148. package/dist/http/upload-with-progress.cjs +1 -1
  149. package/dist/http/upload-with-progress.cjs.map +1 -1
  150. package/dist/http/upload-with-progress.js +17 -17
  151. package/dist/http/upload-with-progress.js.map +1 -1
  152. package/dist/http/use-describe-api-error.cjs +1 -1
  153. package/dist/http/use-describe-api-error.cjs.map +1 -1
  154. package/dist/http/use-describe-api-error.js +9 -9
  155. package/dist/oauth/GoogleSignIn.cjs +1 -1
  156. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  157. package/dist/oauth/GoogleSignIn.js +5 -4
  158. package/dist/oauth/GoogleSignIn.js.map +1 -1
  159. package/dist/offline/create-offline-sync.cjs +1 -1
  160. package/dist/offline/create-offline-sync.cjs.map +1 -1
  161. package/dist/offline/create-offline-sync.js +108 -68
  162. package/dist/offline/create-offline-sync.js.map +1 -1
  163. package/dist/query/offline-persistence.cjs +1 -1
  164. package/dist/query/offline-persistence.cjs.map +1 -1
  165. package/dist/query/offline-persistence.js +54 -44
  166. package/dist/query/offline-persistence.js.map +1 -1
  167. package/dist/sse/create-event-stream.cjs +1 -1
  168. package/dist/sse/create-event-stream.cjs.map +1 -1
  169. package/dist/sse/create-event-stream.js +48 -41
  170. package/dist/sse/create-event-stream.js.map +1 -1
  171. package/dist/sse/ticket-event-stream.cjs +1 -1
  172. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  173. package/dist/sse/ticket-event-stream.js +48 -19
  174. package/dist/sse/ticket-event-stream.js.map +1 -1
  175. package/dist/sse/use-event-stream.cjs +1 -1
  176. package/dist/sse/use-event-stream.cjs.map +1 -1
  177. package/dist/sse/use-event-stream.js +19 -14
  178. package/dist/sse/use-event-stream.js.map +1 -1
  179. package/dist/styles/AuthForm.css +2 -0
  180. package/dist/styles/AuthPage.css +2 -0
  181. package/dist/styles/component/AuthForm.css +9 -0
  182. package/dist/styles/component/AuthPage.css +29 -0
  183. package/dist/styles/forms.css +36 -0
  184. package/dist/styles/manifest.json +63 -0
  185. package/dist/styles.css +1 -1
  186. package/dist/sw/background-sync.cjs +1 -1
  187. package/dist/sw/background-sync.cjs.map +1 -1
  188. package/dist/sw/background-sync.js +132 -112
  189. package/dist/sw/background-sync.js.map +1 -1
  190. package/dist/sw/cache.cjs +1 -1
  191. package/dist/sw/cache.cjs.map +1 -1
  192. package/dist/sw/cache.js +36 -30
  193. package/dist/sw/cache.js.map +1 -1
  194. package/dist/sw/create-push-handler.cjs +1 -1
  195. package/dist/sw/create-push-handler.cjs.map +1 -1
  196. package/dist/sw/create-push-handler.js +20 -10
  197. package/dist/sw/create-push-handler.js.map +1 -1
  198. package/dist/sw.d.ts +93 -28
  199. package/dist/tabular/compact.cjs +1 -1
  200. package/dist/tabular/compact.cjs.map +1 -1
  201. package/dist/tabular/compact.js +35 -16
  202. package/dist/tabular/compact.js.map +1 -1
  203. package/dist/tabular/manifest.cjs +1 -1
  204. package/dist/tabular/manifest.cjs.map +1 -1
  205. package/dist/tabular/manifest.js +33 -15
  206. package/dist/tabular/manifest.js.map +1 -1
  207. package/dist/tabular.d.ts +11 -1
  208. package/dist/tempest-react-sdk.cjs +1 -1
  209. package/dist/tempest-react-sdk.d.ts +1057 -83
  210. package/dist/tempest-react-sdk.js +373 -365
  211. package/dist/theme/initial-theme.cjs +4 -4
  212. package/dist/theme/initial-theme.cjs.map +1 -1
  213. package/dist/theme/initial-theme.js +8 -5
  214. package/dist/theme/initial-theme.js.map +1 -1
  215. package/dist/utils/compressed-storage.cjs +1 -1
  216. package/dist/utils/compressed-storage.cjs.map +1 -1
  217. package/dist/utils/compressed-storage.js +13 -7
  218. package/dist/utils/compressed-storage.js.map +1 -1
  219. package/dist/utils/format.cjs +1 -1
  220. package/dist/utils/format.cjs.map +1 -1
  221. package/dist/utils/format.js +25 -25
  222. package/dist/utils/numbers.cjs +1 -1
  223. package/dist/utils/numbers.cjs.map +1 -1
  224. package/dist/utils/numbers.js +7 -7
  225. package/dist/utils/objects.cjs +1 -1
  226. package/dist/utils/objects.cjs.map +1 -1
  227. package/dist/utils/objects.js +13 -7
  228. package/dist/utils/objects.js.map +1 -1
  229. package/dist/utils/safe-url.cjs +1 -1
  230. package/dist/utils/safe-url.cjs.map +1 -1
  231. package/dist/utils/safe-url.js +4 -1
  232. package/dist/utils/safe-url.js.map +1 -1
  233. package/dist/webrtc/peer-mesh.cjs +1 -1
  234. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  235. package/dist/webrtc/peer-mesh.js +7 -1
  236. package/dist/webrtc/peer-mesh.js.map +1 -1
  237. package/dist/ws/create-web-socket.cjs +1 -1
  238. package/dist/ws/create-web-socket.cjs.map +1 -1
  239. package/dist/ws/create-web-socket.js +77 -61
  240. package/dist/ws/create-web-socket.js.map +1 -1
  241. package/dist/ws/use-web-socket.cjs +1 -1
  242. package/dist/ws/use-web-socket.cjs.map +1 -1
  243. package/dist/ws/use-web-socket.js +4 -3
  244. package/dist/ws/use-web-socket.js.map +1 -1
  245. package/package.json +1 -1
  246. package/template-pwa/src/pwa.tsx +42 -0
  247. package/template-pwa/src/sw.ts +15 -10
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # tempest-react-sdk
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/tempest-react-sdk.svg)](https://www.npmjs.com/package/tempest-react-sdk)
4
- [![CI](https://github.com/mauriciobenjamin700/tempest-react-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mauriciobenjamin700/tempest-react-sdk/actions/workflows/ci.yml)
5
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
6
5
  [![React 18 / 19](https://img.shields.io/badge/react-18%20%7C%2019-61dafb.svg?logo=react)](https://react.dev)
7
6
  [![TypeScript](https://img.shields.io/badge/types-TypeScript-3178c6.svg?logo=typescript)](https://www.typescriptlang.org/)
@@ -43,6 +42,7 @@ The goal is to start every new React frontend with the same opinionated foundati
43
42
  - [Polling](#polling-recipe)
44
43
  - [Retry & idempotency](#retry--idempotency-recipe)
45
44
  - [Auth store (Zustand)](#auth-store-recipe)
45
+ - [Auth screens (login, sign-up, password reset)](#auth-screens-recipe)
46
46
  - [Route guard](#route-guard-recipe)
47
47
  - [JWT helpers & refresh queue](#jwt-helpers--refresh-queue-recipe)
48
48
  - [Code-splitting with retry](#code-splitting-with-retry-recipe)
@@ -159,7 +159,7 @@ Via `package.json`:
159
159
 
160
160
  Requires React `>=18` and Node `>=22.12` to build.
161
161
 
162
- **A bundler is assumed — outside one, load the shipped `.css` loader.** Every component module imports its own stylesheet (see [CSS import](#css-import)), and `.css` is not a format Node can load — `node -e "import('tempest-react-sdk')"` fails with `ERR_UNKNOWN_FILE_EXTENSION`. Vite, webpack, Rspack, Parcel and esbuild all resolve that import, and the SDK is client-side only, so no real app consumer is affected. For everything that is not a bundler — a script, a Jest suite, reading the surface by introspection — the package ships the loader: `node --import tempest-react-sdk/node-css-loader your-script.mjs` resolves every stylesheet to an empty module, for `import` and `require` both. A **Jest** suite can use it or the usual `moduleNameMapper: { "\\.css$": "<rootDir>/test/css-stub.js" }`. Vitest needs nothing.
162
+ **A bundler is assumed — outside one, load the shipped `.css` loader.** Every component module imports its own stylesheet (see [CSS import](#css-import)), and `.css` is not a format Node can load — `node -e "import('tempest-react-sdk')"` fails with `ERR_UNKNOWN_FILE_EXTENSION`. Vite, webpack, Rspack, Parcel and esbuild all resolve that import, and the SDK is client-side only, so no real app consumer is affected. For everything that is not a bundler — a script, a Jest suite, reading the surface by introspection — the package ships the loader: `node --import tempest-react-sdk/node-css-loader your-script.mjs` resolves every stylesheet to an empty module, for `import` and `require` both. A **Jest** suite can use it or the usual `moduleNameMapper: { "\\.css$": "<rootDir>/test/css-stub.js" }`. **Vitest** needs `tempestVitest()` from `tempest-react-sdk/vite`, because it hands every ESM package from `node_modules` to Node: `createViteConfig` already wires it, but a separate `vitest.config.ts` (or a plain `vite.config.ts`) must add it to `plugins`, or the first import of the SDK fails with `Unknown file extension ".css"` — see [Testing with Vitest](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/vite-config/#testing-with-vitest).
163
163
 
164
164
  ### Peer & bundled dependencies
165
165
 
@@ -267,7 +267,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
267
267
 
268
268
  | Module | Exports |
269
269
  | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
270
- | `components` | `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `InstallBanner`, `InstallButton`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
270
+ | `components` | `AuthPage` + `AuthForm` with the presets `LoginForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm` (auth screens from a list of fields), `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `InstallBanner`, `InstallButton`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
271
271
  | `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useDraftFilters` (draft vs applied server-side filter), `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useAnnounce` (+ pure `announce`/`clearAnnouncer` — one shared live-region pair), `useDeepMemo`, `useObjectUrl`, `useLongPressHandlers` (spreadable long-press handlers + `wasLongPress()` guard), `useInstallPrompt` (install-method resolver with decline cooldown), `useServiceWorkerUpdate` (consent-based SW update flow), `useStorageEstimate` + `estimateStorage`/`requestPersistentStorage` (Storage API quota & persistence), PWA env helpers `isIOS`/`isAndroid`/`isAndroidWithoutPromptApi`/`isStandalone`/`buildOpenInChromeIntent`, type `BeforeInstallPromptEvent` |
272
272
  | `http` | `createApiClient`, `buildApiUrl`, `csrfHeaders`, `parseResponse`, `uploadWithProgress`, `createResumableUpload` (tus 1.0.0: chunked + resumable, `pause`/`resume`/`abort`), `createLocalUploadStorage`, `uploadFingerprint`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `BuildApiUrlOptions`, `CsrfOptions`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
273
273
  | `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, **passkeys/WebAuthn**: `createPasskeyClient`, `usePasskeyRegistration`, `usePasskeySignIn`, `usePasskeyCapabilities`, `isPasskeySupported`, `isPlatformAuthenticatorAvailable`, `isConditionalMediationAvailable`, `classifyPasskeyError`, `PasskeyError`, `base64UrlToBytes`, `bytesToBase64Url`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
@@ -284,7 +284,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
284
284
  | `geo` _(opt. peer `leaflet`)_ | `haversineKm`, `pathLengthKm`, `bearingDeg`, `estimateTravel`, `createOSRMBackend`, `boundingBox`, `projectMercator`, `fitProjection`, `createPositionTracker`, `usePositionTracker`, `TrajectoryMap` — tile-free SVG map + trajectory tracking (no external/paid API; Leaflet is an opt-in, lazy-loaded tile layer for self-hosted tiles), types: `Coordinate`, `TrackPoint`, `TravelEstimate`, `TravelMode`, `GeoBounds`, `RoutingBackend` |
285
285
  | `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
286
286
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
287
- | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`, credentials never stored), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
287
+ | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`, credentials never stored and re-applied only to the worker's origin or `trustedOrigins`), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
288
288
  | `charts` _(subpath `tempest-react-sdk/charts`, opt. peer `recharts`)_ | `AreaChart`, `BarChart`, `LineChart`, `PieChart`, `RadarChart`, `DEFAULT_CHART_COLORS` — themed recharts wrappers (recharts externalized; install it only if you use charts) |
289
289
  | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
290
290
  | `neural` _(subpath `tempest-react-sdk/neural`, opt. peer `@sapphi-red/web-noise-suppressor`)_ | `isNeuralSuppressionSupported`, `prepareNeuralSuppressor`, `createNeuralSuppressor`, types `NeuralSuppressorAssets`, `NeuralSuppressor` — RNNoise noise suppression: a recurrent model in an `AudioWorklet` separates speech from non-speech on a 48 kHz context and hands back a denoised `MediaStreamTrack` to publish in place of the capture (peer externalized; install it only if you use neural) |
@@ -321,7 +321,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
321
321
  | `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
322
322
  | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
323
323
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
324
- | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`, credentials never stored), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
324
+ | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`, credentials never stored and re-applied only to the worker's origin or `trustedOrigins`), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
325
325
  | `editor` _(subpath `tempest-react-sdk/editor`, opt. peers `@tiptap/react`+`@tiptap/starter-kit`)_ | `RichTextEditor` — themed tiptap v3 rich-text editor with toolbar (tiptap externalized; install it only if you use the editor) |
326
326
  | `vision` _(subpath `tempest-react-sdk/vision`, opt. peer `onnxruntime-web`)_ | `Classifier`, `Detector`, `Segmenter`, `DetectClassify` (fused detect→classify pipeline, optionally with a segmentation stage) + `warmup()`, FP16 models, `cache` (Cache Storage), video/`VideoFrame` inputs, `raiseOnEmpty`/`NoDetectionsError`, `LetterboxPipeline`, `readFusionSpec` and results/labels/preprocess — on-device ONNX vision inference, vendored from `@mauriciobenjamin700/ort-vision-sdk-web` (onnxruntime-web externalized; install it only if you use vision). Plus browser capture primitives: `useCameraStream` (rear-camera `getUserMedia` + classified errors + `retry()`), `computeImageLuminance`/`isLuminanceAcceptable`/`LowLuminanceError`/`LUMINANCE_SAMPLE_MAX_EDGE` (BT.709 brightness), `useLiveLuminance` (live rAF sampling) |
327
327
  | `tabular` _(subpath `tempest-react-sdk/tabular`, opt. peer `onnxruntime-web`)_ | `TabularPredictor`, `useTabularPredictor` — scikit-learn models exported by `tempest-fastapi-sdk`'s `export_sklearn_to_onnx`, running in the browser with no inference server. Offline by design: `fetchModelBytes`/`isModelCached`/`cacheModelBytes`/`clearModelCache` (Cache Storage, cache-first) plus `configureOrtAssets`/`ortAssetUrls`/`ORT_WASM_ASSETS` for the runtime's WebAssembly binaries, which ONNX Runtime Web fetches at session creation. Import the default `onnxruntime-web` entry — the WebGPU build has no `ai.onnx.ml` kernels |
@@ -778,6 +778,37 @@ useAuthStore.getState().logout();
778
778
 
779
779
  The store exposes `user`, `token`, `isAuthenticated`, `setSession`, `setUser`, `setToken`, and `logout`. `isAuthenticated` is derived from `token` and rehydrates correctly after page reload.
780
780
 
781
+ ### Auth screens recipe
782
+
783
+ The sign-in screen without hand-building the layout or the form: `AuthPage` is the shell (colored top + rounded sheet below 768 px, 50/50 brand panel and form above it, or a centered `card`), and `LoginForm` / `SignUpForm` / `ForgotPasswordForm` / `ResetPasswordForm` are generated from a list of fields and a zod schema. A 422 with `fields` lands on the matching inputs; any other error shows an `Alert`.
784
+
785
+ ```tsx
786
+ import { AuthPage, LoginForm, createApiClient, useNavigate } from "tempest-react-sdk";
787
+
788
+ const api = createApiClient({ baseURL: "/api" });
789
+
790
+ export function LoginScreen() {
791
+ const navigate = useNavigate();
792
+ return (
793
+ <AuthPage
794
+ brand={<img src="/logo.svg" alt="Acme" width={96} height={96} />}
795
+ back="/"
796
+ title="Bem-vindo!"
797
+ >
798
+ <LoginForm
799
+ onSubmit={async (values) => {
800
+ await api.post("/auth/login", { body: values });
801
+ navigate("/", { replace: true });
802
+ }}
803
+ links={{ forgotPassword: "/recuperar-senha", signUp: "/cadastro" }}
804
+ />
805
+ </AuthPage>
806
+ );
807
+ }
808
+ ```
809
+
810
+ Full guide: [Auth screens](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/components/auth-screens/) · [Telas de autenticação](https://mauriciobenjamin700.github.io/tempest-react-sdk/components/auth-screens/).
811
+
781
812
  ### Route guard recipe
782
813
 
783
814
  ```tsx
@@ -1964,15 +1995,14 @@ Every gallery section maps 1-to-1 with a docs page — see [`docs/gallery.md`](.
1964
1995
 
1965
1996
  ## Release
1966
1997
 
1967
- Versioning is managed with [Changesets](https://github.com/changesets/changesets):
1998
+ Two commands, with the release PR's review in between:
1968
1999
 
1969
2000
  ```bash
1970
- npx changeset # describe the change (patch / minor / major)
1971
- npx changeset version # bump versions + update CHANGELOG.md
1972
- npm run release # build + changeset publish
2001
+ make release TAG=X.Y.Z # opens the release PR (no tag, no publish)
2002
+ make release-tag TAG=X.Y.Z # after the merge: tags the merge commit and pushes it
1973
2003
  ```
1974
2004
 
1975
- CI workflow `.github/workflows/release.yml` publishes to npm whenever a "Version Packages" PR merges into `main`. The first publish is manual — set the `NPM_TOKEN` secret in _Settings → Secrets and variables → Actions_ and run `npm publish` once.
2005
+ The tag push runs `.github/workflows/release-npm.yml`, which publishes through npm Trusted Publishing (OIDC, with provenance) — no npm token anywhere, and no local publish (`make publish` refuses). Pipeline and the repository owner's settings checklist: [Release pipeline](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/release/).
1976
2006
 
1977
2007
  Tags follow `vMAJOR.MINOR.PATCH`. CSS tokens (`--tempest-*`) are treated as public API — renames bump the minor (or major, on removal).
1978
2008
 
@@ -49,6 +49,37 @@ function scaffoldHere() {
49
49
  return { status: result.status, stdout: result.stdout + result.stderr };
50
50
  }
51
51
 
52
+ describe("create-tempest-app new-folder mode refuses a destination that is a symlink (#533)", () => {
53
+ it("my-app -> ../outside is refused and nothing lands at the link target", () => {
54
+ symlinkSync("../outside", join(proj, "my-app"));
55
+
56
+ const result = spawnSync(process.execPath, [CLI, "my-app"], {
57
+ cwd: proj,
58
+ encoding: "utf8",
59
+ env: { ...process.env, NO_COLOR: "1" },
60
+ });
61
+
62
+ expect(result.status).toBe(1);
63
+ expect(result.stderr).toMatch(/"my-app" is a symbolic link/);
64
+ expect(readdirSync(outside)).toEqual([]);
65
+ expect(lstatSync(join(proj, "my-app")).isSymbolicLink()).toBe(true);
66
+ });
67
+
68
+ it("a plain new folder name is still scaffolded", () => {
69
+ const result = spawnSync(process.execPath, [CLI, "my-app"], {
70
+ cwd: proj,
71
+ encoding: "utf8",
72
+ env: { ...process.env, NO_COLOR: "1" },
73
+ });
74
+
75
+ expect(result.status).toBe(0);
76
+ expect(existsSync(join(proj, "my-app", "src", "main.tsx"))).toBe(true);
77
+ expect(JSON.parse(readFileSync(join(proj, "my-app", "package.json"), "utf8")).name).toBe(
78
+ "my-app",
79
+ );
80
+ });
81
+ });
82
+
52
83
  describe("create-tempest-app merge mode never writes through a symlink (#436)", () => {
53
84
  it("a symlinked src/ pointing outside the project is skipped, not written into", () => {
54
85
  writeFileSync(join(outside, "main.tsx"), "OUTSIDE-USER-FILE");
@@ -10,7 +10,8 @@
10
10
  // package.json has the Tempest scripts/deps merged in (your name/version are
11
11
  // preserved). A symlink already in the destination is never written through:
12
12
  // it is skipped and reported, so a link to a folder outside the project cannot
13
- // redirect the template there. The `--pwa` flag overlays the PWA template
13
+ // redirect the template there. A named destination that is itself a symlink is
14
+ // refused for the same reason. The `--pwa` flag overlays the PWA template
14
15
  // (manifest, service worker, push-subscribe wiring) on top of the base.
15
16
  import { cp, lstat, mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
16
17
  import { existsSync } from "node:fs";
@@ -239,6 +240,15 @@ async function main() {
239
240
  process.exit(1);
240
241
  }
241
242
  destDir = resolve(process.cwd(), arg);
243
+ if (await isSymlink(destDir)) {
244
+ console.error(
245
+ `${c.red}✗ "${arg}" is a symbolic link; the project would be created where it points.${c.reset}`,
246
+ );
247
+ console.error(
248
+ `${c.dim} Create the project at the real path, or remove the link.${c.reset}`,
249
+ );
250
+ process.exit(1);
251
+ }
242
252
  if (!(await isEmptyDir(destDir))) {
243
253
  console.error(`${c.red}✗ Directory "${arg}" exists and is not empty.${c.reset}`);
244
254
  console.error(
@@ -15,10 +15,22 @@ import { fileURLToPath } from "node:url";
15
15
 
16
16
  import { ESLint } from "eslint";
17
17
  import ts from "typescript";
18
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
19
 
20
20
  import { componentFiles, normalizeComponentName } from "./generate.mjs";
21
21
 
22
+ /**
23
+ * Gives the tests in this file 60 s instead of Vitest's 5 s default.
24
+ *
25
+ * Several of them build a TypeScript program (and, some, an ESLint instance)
26
+ * in-process over the generated files. That takes 3-5 s on an idle machine,
27
+ * right at the default, so a busy one — another suite, a model training — failed
28
+ * them by timeout while the code was correct: 5.1-5.7 s measured in three
29
+ * `make check` runs on 2026-11-01, load average between 21 and 48.
30
+ * `test/docs-guard.test.ts` gives its compile 120 s for the same reason.
31
+ */
32
+ vi.setConfig({ testTimeout: 60_000 });
33
+
22
34
  const HERE = dirname(fileURLToPath(import.meta.url));
23
35
  const REPO = resolve(HERE, "..", "..", "..");
24
36
  const CLI = join(REPO, "bin", "tempest.mjs");
@@ -98,6 +98,11 @@ describe("tempest doctor — a third-party project that does not use the SDK", (
98
98
  expect(out).not.toContain('add import "tempest-react-sdk/styles.css"');
99
99
  });
100
100
 
101
+ it("does not judge the Vitest config of a project without the SDK", () => {
102
+ installed("vitest", "4.1.12");
103
+ expect(doctor().out).not.toContain("inline the SDK");
104
+ });
105
+
101
106
  it("does not demand a src/main.tsx it has no reason to expect", () => {
102
107
  const { out } = doctor();
103
108
  expect(out).not.toContain("app entry");
@@ -336,6 +341,30 @@ describe("tempest doctor — a project that does use the SDK", () => {
336
341
  expect(doctor().out).toContain("leaflet missing");
337
342
  });
338
343
 
344
+ it("warns when Vitest would load the SDK without the plugin", () => {
345
+ installed("vitest", "4.1.12");
346
+ const { out, code } = doctor();
347
+ expect(out).toContain("vite.config.ts does not inline the SDK for Vitest");
348
+ expect(out).toContain("tempestVitest()");
349
+ expect(code).toBe(0);
350
+ });
351
+
352
+ it("accepts a vitest.config that adds tempestVitest", () => {
353
+ installed("vitest", "4.1.12");
354
+ write(
355
+ "vitest.config.ts",
356
+ 'import { tempestVitest } from "tempest-react-sdk/vite";\n' +
357
+ "export default defineConfig({ plugins: [tempestVitest()] });",
358
+ );
359
+ const { out } = doctor();
360
+ expect(out).toContain("vitest.config.ts lets Vitest load the SDK");
361
+ expect(out).not.toContain("does not inline the SDK");
362
+ });
363
+
364
+ it("says nothing about Vitest in a project that does not install it", () => {
365
+ expect(doctor().out).not.toContain("Vitest");
366
+ });
367
+
339
368
  it("warns when the SDK is installed but undeclared", () => {
340
369
  write("package.json", {
341
370
  name: "sdk-app",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The Vitest config check for `tempest doctor`.
3
+ *
4
+ * Every SDK component module imports its own stylesheet, and Vitest hands an ESM
5
+ * package from `node_modules` to plain Node, which has no loader for `.css`. A
6
+ * consumer suite that imports the SDK therefore dies before running with
7
+ * `Unknown file extension ".css"` unless the config inlines the SDK — which is what
8
+ * `tempestVitest()` does, and what `createViteConfig` includes. Measured in #548
9
+ * with the 0.81.0 tarball on Vitest 4.1.12: an own `vitest.config.ts`, a plain
10
+ * `vite.config.ts` and no config at all fail; only the plugin passes.
11
+ *
12
+ * The check is a text heuristic over the config file Vitest would load, not an
13
+ * evaluation of it: running the user's config would execute their code and pull
14
+ * their plugins in, which a health check has no business doing. The markers are the
15
+ * plugin and helper names, an explicit `inline: true` (which inlines everything,
16
+ * the SDK included), and an inline list naming the package — the three shapes that
17
+ * make the suite pass.
18
+ */
19
+
20
+ /**
21
+ * Config file names Vitest looks for, in the order it prefers them.
22
+ *
23
+ * Ported from Vitest 4.1.12's `configFiles` (`CONFIG_NAMES` × `CONFIG_EXTENSIONS`
24
+ * in `dist/chunks/constants.*.js`, not exported): a `vitest.config.*` wins over a
25
+ * `vite.config.*`, and within each the extensions are tried in this order. The
26
+ * test reads the installed Vitest and fails when the list drifts.
27
+ */
28
+ export const VITEST_CONFIG_FILES = [
29
+ "vitest.config.ts",
30
+ "vitest.config.mts",
31
+ "vitest.config.cts",
32
+ "vitest.config.js",
33
+ "vitest.config.mjs",
34
+ "vitest.config.cjs",
35
+ "vite.config.ts",
36
+ "vite.config.mts",
37
+ "vite.config.cts",
38
+ "vite.config.js",
39
+ "vite.config.mjs",
40
+ "vite.config.cjs",
41
+ ];
42
+
43
+ /** Text that, once comments are gone, means the config already inlines the SDK. */
44
+ const INLINES_SDK = [
45
+ /\btempestVitest\b/,
46
+ /\bcreateViteConfig\b/,
47
+ /\binline\s*:\s*true\b/,
48
+ /\binline\s*:\s*\[[^\]]*["'`]tempest-react-sdk["'`]/,
49
+ ];
50
+
51
+ /** An import of the project's own `vite.config`, as `mergeConfig` setups do. */
52
+ const IMPORTS_VITE_CONFIG = /from\s*["']\.\/vite\.config(?:\.[cm]?[jt]s)?["']/;
53
+
54
+ /**
55
+ * Drop JavaScript comments so a marker that only appears in prose does not count.
56
+ *
57
+ * A line comment is only removed when `//` is not preceded by `:`, which keeps
58
+ * `https://` inside a string intact; a missed edge case here costs one false "ok",
59
+ * never a crash.
60
+ *
61
+ * @param {string} text - Source text of a config file.
62
+ * @returns {string} The text without block and line comments.
63
+ */
64
+ function stripComments(text) {
65
+ return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
66
+ }
67
+
68
+ /**
69
+ * Whether a config file's text makes Vitest inline the SDK.
70
+ *
71
+ * @param {string} text - Source text of the config.
72
+ * @returns {boolean} Whether one of the passing shapes is present.
73
+ */
74
+ function inlinesSdk(text) {
75
+ const code = stripComments(text);
76
+ return INLINES_SDK.some((marker) => marker.test(code));
77
+ }
78
+
79
+ /**
80
+ * Audit whether the project's Vitest setup can import the SDK.
81
+ *
82
+ * Vitest loads the first `vitest.config.*`, else the first `vite.config.*`, else
83
+ * runs with defaults; this mirrors that choice and judges only the file it would
84
+ * load. A `vitest.config.*` that imports `./vite.config` (the `mergeConfig`
85
+ * pattern) passes when that `vite.config.*` does, so a project using
86
+ * `createViteConfig` there is not flagged.
87
+ *
88
+ * @param {object} params
89
+ * @param {boolean} params.vitestInstalled - Whether `vitest` resolves in the project.
90
+ * @param {Record<string, string>} params.configs - Text of each config file present,
91
+ * keyed by its name from {@link VITEST_CONFIG_FILES}.
92
+ * @returns {Array<[status: string, label: string, detail?: string]>} Check rows;
93
+ * empty when the project does not use Vitest.
94
+ */
95
+ export function checkVitest({ vitestInstalled, configs }) {
96
+ if (!vitestInstalled) return [];
97
+
98
+ const loaded = VITEST_CONFIG_FILES.find((name) => name in configs);
99
+ const fix =
100
+ "add `plugins: [tempestVitest()]` from tempest-react-sdk/vite, or build the config " +
101
+ "with createViteConfig — otherwise any test importing the SDK fails with " +
102
+ '`Unknown file extension ".css"`';
103
+
104
+ if (!loaded) {
105
+ return [["warn", "Vitest runs with no config", fix]];
106
+ }
107
+
108
+ const text = configs[loaded];
109
+ const viaViteConfig =
110
+ loaded.startsWith("vitest.") &&
111
+ IMPORTS_VITE_CONFIG.test(stripComments(text)) &&
112
+ VITEST_CONFIG_FILES.some(
113
+ (name) => name.startsWith("vite.") && name in configs && inlinesSdk(configs[name]),
114
+ );
115
+
116
+ if (inlinesSdk(text) || viaViteConfig) {
117
+ return [["ok", `${loaded} lets Vitest load the SDK`]];
118
+ }
119
+ return [["warn", `${loaded} does not inline the SDK for Vitest`, fix]];
120
+ }
@@ -0,0 +1,139 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { VITEST_CONFIG_FILES, checkVitest } from "./vitest.mjs";
7
+
8
+ const VITEST_CHUNKS = join(
9
+ dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ "..",
12
+ "..",
13
+ "node_modules",
14
+ "vitest",
15
+ "dist",
16
+ "chunks",
17
+ );
18
+
19
+ /** Statuses of the returned rows, in order. */
20
+ const statuses = (rows) => rows.map(([status]) => status);
21
+
22
+ /** Run the check for a project with Vitest and the given config files. */
23
+ const check = (configs) => checkVitest({ vitestInstalled: true, configs });
24
+
25
+ /**
26
+ * The config file list the installed Vitest really searches, read from its source.
27
+ *
28
+ * @returns {string[]} `CONFIG_NAMES` × `CONFIG_EXTENSIONS`, in Vitest's order.
29
+ */
30
+ function installedVitestConfigFiles() {
31
+ const file = readdirSync(VITEST_CHUNKS).find((name) => /^constants\..*\.js$/.test(name));
32
+ const text = readFileSync(join(VITEST_CHUNKS, file), "utf8");
33
+ const array = (name) =>
34
+ JSON.parse(
35
+ new RegExp(`const ${name} = (\\[[^\\]]*\\])`)
36
+ .exec(text)[1]
37
+ .replace(/\s+/g, "")
38
+ .replace(/,]/, "]"),
39
+ );
40
+ return array("CONFIG_NAMES").flatMap((name) =>
41
+ array("CONFIG_EXTENSIONS").map((ext) => name + ext),
42
+ );
43
+ }
44
+
45
+ describe("checkVitest — which file it judges", () => {
46
+ it("searches the same files, in the same order, as the installed Vitest", () => {
47
+ expect(VITEST_CONFIG_FILES).toEqual(installedVitestConfigFiles());
48
+ });
49
+
50
+ it("judges vitest.config over vite.config, the one Vitest loads", () => {
51
+ const rows = check({
52
+ "vite.config.ts": "export default createViteConfig();",
53
+ "vitest.config.ts": "export default defineConfig({ test: {} });",
54
+ });
55
+ expect(statuses(rows)).toEqual(["warn"]);
56
+ expect(rows[0][1]).toContain("vitest.config.ts");
57
+ });
58
+
59
+ it("falls back to vite.config when there is no vitest.config", () => {
60
+ const rows = check({ "vite.config.mjs": "export default defineConfig({});" });
61
+ expect(rows[0][1]).toContain("vite.config.mjs");
62
+ });
63
+ });
64
+
65
+ describe("checkVitest — setups that fail the suite", () => {
66
+ it("says nothing when the project does not use Vitest", () => {
67
+ expect(checkVitest({ vitestInstalled: false, configs: {} })).toEqual([]);
68
+ });
69
+
70
+ it("warns when Vitest would run with no config at all", () => {
71
+ const rows = check({});
72
+ expect(statuses(rows)).toEqual(["warn"]);
73
+ expect(rows[0][2]).toContain("tempestVitest()");
74
+ expect(rows[0][2]).toContain('Unknown file extension ".css"');
75
+ });
76
+
77
+ it("warns on a plain vitest.config without the plugin", () => {
78
+ const rows = check({
79
+ "vitest.config.ts":
80
+ 'import { defineConfig } from "vitest/config";\n' +
81
+ 'export default defineConfig({ test: { environment: "jsdom" } });',
82
+ });
83
+ expect(statuses(rows)).toEqual(["warn"]);
84
+ });
85
+
86
+ it("does not count the plugin named only in a comment", () => {
87
+ const rows = check({
88
+ "vitest.config.ts":
89
+ "// TODO: add tempestVitest()\n/* createViteConfig later */\n" +
90
+ "export default defineConfig({});",
91
+ });
92
+ expect(statuses(rows)).toEqual(["warn"]);
93
+ });
94
+
95
+ it("does not trust an import of a vite.config that lacks the plugin too", () => {
96
+ const rows = check({
97
+ "vite.config.ts": "export default defineConfig({});",
98
+ "vitest.config.ts":
99
+ 'import viteConfig from "./vite.config";\n' +
100
+ "export default mergeConfig(viteConfig, defineConfig({ test: {} }));",
101
+ });
102
+ expect(statuses(rows)).toEqual(["warn"]);
103
+ });
104
+ });
105
+
106
+ describe("checkVitest — setups that pass", () => {
107
+ it.each([
108
+ ["tempestVitest()", "export default defineConfig({ plugins: [tempestVitest()] });"],
109
+ ["createViteConfig", "export default createViteConfig({ overrides: {} });"],
110
+ [
111
+ "inline: true",
112
+ "export default defineConfig({ test: { server: { deps: { inline: true } } } });",
113
+ ],
114
+ [
115
+ "an inline list naming the SDK",
116
+ 'export default defineConfig({ test: { server: { deps: { inline: ["x", "tempest-react-sdk"] } } } });',
117
+ ],
118
+ ])("accepts %s", (_, text) => {
119
+ expect(statuses(check({ "vitest.config.ts": text }))).toEqual(["ok"]);
120
+ });
121
+
122
+ it("accepts a vitest.config merged over a vite.config that uses createViteConfig", () => {
123
+ const rows = check({
124
+ "vite.config.ts": "export default createViteConfig();",
125
+ "vitest.config.ts":
126
+ 'import viteConfig from "./vite.config.ts";\n' +
127
+ "export default mergeConfig(viteConfig, defineConfig({ test: {} }));",
128
+ });
129
+ expect(statuses(rows)).toEqual(["ok"]);
130
+ });
131
+
132
+ it("keeps a URL inside a string from hiding a marker after it", () => {
133
+ const rows = check({
134
+ "vitest.config.ts":
135
+ 'const docs = "https://example.com"; export default defineConfig({ plugins: [tempestVitest()] });',
136
+ });
137
+ expect(statuses(rows)).toEqual(["ok"]);
138
+ });
139
+ });
@@ -28,6 +28,13 @@ export function lstatOrNull(path) {
28
28
  * not skipped over: its `realpath` throws and the path counts as outside, since
29
29
  * `mkdir` or `writeFile` would create whatever the link names.
30
30
  *
31
+ * The answer is about the tree as it is now; nothing makes it and the write that
32
+ * follows one atomic step. A process swapping a folder for a symlink between the
33
+ * two can still redirect the write, and Node has no `openat`/`O_NOFOLLOW` walk
34
+ * to close that. Such a process is already writing inside the user's project, so
35
+ * it could edit the generated files or `package.json` directly: the window is an
36
+ * accepted risk, the same one `create-tempest-app` documents (#533).
37
+ *
31
38
  * @param {string} root - Project root.
32
39
  * @param {string} path - Absolute path, existing or about to be created.
33
40
  * @returns {boolean}