tempest-react-sdk 0.5.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -35
- package/bin/create-tempest-app.mjs +248 -0
- package/bin/tempest.mjs +282 -0
- package/dist/styles.css +1 -1
- package/dist/sw.cjs +2 -0
- package/dist/sw.cjs.map +1 -0
- package/dist/sw.d.ts +103 -0
- package/dist/sw.js +95 -0
- package/dist/sw.js.map +1 -0
- package/dist/tempest-react-sdk.cjs +4 -4
- package/dist/tempest-react-sdk.cjs.map +1 -1
- package/dist/tempest-react-sdk.d.ts +1697 -7
- package/dist/tempest-react-sdk.js +4485 -2989
- package/dist/tempest-react-sdk.js.map +1 -1
- package/dist/vite.cjs +2 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.ts +62 -0
- package/dist/vite.js +46 -0
- package/dist/vite.js.map +1 -0
- package/package.json +34 -2
- package/template/README.md +37 -0
- package/template/_env.example +2 -0
- package/template/_gitignore +9 -0
- package/template/_prettierrc.json +9 -0
- package/template/eslint.config.js +44 -0
- package/template/index.html +12 -0
- package/template/package.json +38 -0
- package/template/src/App.tsx +15 -0
- package/template/src/layouts/RootLayout.tsx +28 -0
- package/template/src/lib/api.ts +17 -0
- package/template/src/main.tsx +10 -0
- package/template/src/pages/Dashboard.tsx +19 -0
- package/template/src/pages/Home.tsx +16 -0
- package/template/src/pages/Login.tsx +27 -0
- package/template/src/routes.tsx +27 -0
- package/template/src/stores/auth.ts +15 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +26 -0
- package/template/vite.config.ts +7 -0
- package/template-pwa/README.md +64 -0
- package/template-pwa/_env.example +7 -0
- package/template-pwa/index.html +22 -0
- package/template-pwa/package.json +6 -0
- package/template-pwa/public/icon-maskable.svg +4 -0
- package/template-pwa/public/icon.svg +4 -0
- package/template-pwa/public/manifest.webmanifest +35 -0
- package/template-pwa/src/main.tsx +36 -0
- package/template-pwa/src/pages/Dashboard.tsx +73 -0
- package/template-pwa/src/sw.ts +35 -0
- package/template-pwa/src/vite-env.d.ts +12 -0
- package/template-pwa/vite.sw.config.ts +27 -0
package/README.md
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://bundlephobia.com/package/tempest-react-sdk)
|
|
9
9
|
|
|
10
|
+
[**📖 Documentação completa (PT-BR) →**](https://mauriciobenjamin700.github.io/tempest-react-sdk/) · [**📖 Full documentation (EN-US) →**](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)
|
|
11
|
+
|
|
12
|
+
> O site MkDocs é bilíngue (**PT-BR** padrão · **EN-US**) com seletor de idioma 🇧🇷/🇺🇸 no cabeçalho. — The MkDocs site is bilingual (**PT-BR** default · **EN-US**) with a 🇧🇷/🇺🇸 language switcher in the header. The site is the navigable, per-module source of truth; this README stays the npm/GitHub landing page.
|
|
13
|
+
|
|
14
|
+
- **PT-BR:** [Scaffold](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/) · [Estrutura de app](https://mauriciobenjamin700.github.io/tempest-react-sdk/architecture/) · [Componentes](https://mauriciobenjamin700.github.io/tempest-react-sdk/components/) · [Hooks](https://mauriciobenjamin700.github.io/tempest-react-sdk/hooks/) · [Utilitários](https://mauriciobenjamin700.github.io/tempest-react-sdk/utilities/)
|
|
15
|
+
- **EN-US:** [Scaffold](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/scaffold/) · [App foundation](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/architecture/) · [Components](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/components/) · [Hooks](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/hooks/) · [Utilities](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/utilities/)
|
|
16
|
+
- **🤖 For LLMs:** [llms.txt](https://mauriciobenjamin700.github.io/tempest-react-sdk/llms.txt) (curated index) · [llms-full.txt](https://mauriciobenjamin700.github.io/tempest-react-sdk/llms-full.txt) (full docs in one block) — [llmstxt.org](https://llmstxt.org) convention.
|
|
17
|
+
|
|
18
|
+
> 💡 `pip install -r docs/requirements.txt && mkdocs serve` é só para preview local — em produção use as URLs do GitHub Pages acima. / For local preview only — in production use the GitHub Pages URLs above.
|
|
19
|
+
|
|
10
20
|
Shared React/TypeScript building blocks used across Tempest frontends: UI components, hooks, HTTP client, auth store, query keys, forms (zod), real-time transports (SSE / WebSocket / Web Push / Service Worker), theme, i18n, telemetry, feature flags, offline storage, error boundary, and a curated set of utilities (`cn`, `formatCurrency`, `formatCPF`, etc.).
|
|
11
21
|
|
|
12
22
|
The goal is to start every new React frontend with the same opinionated foundation already in place — no copy-pasting `Button`/`Input` styles, no rewriting the same auth Zustand store, no re-inventing the SSE reconnect loop. The patterns here are a distillation of what was consolidated in **alofans-frontend** and **transport-admin-system** — apps that consume the SDK gain consistency without paying for boilerplate.
|
|
@@ -20,6 +30,8 @@ The goal is to start every new React frontend with the same opinionated foundati
|
|
|
20
30
|
- [Peer & bundled dependencies](#peer--bundled-dependencies)
|
|
21
31
|
- [CSS import](#css-import)
|
|
22
32
|
- [What's inside](#whats-inside)
|
|
33
|
+
- [Scaffold a new app](#scaffold-a-new-app)
|
|
34
|
+
- [App foundation (routing, state, providers, Vite)](#app-foundation)
|
|
23
35
|
- [Architecture overview](#architecture-overview)
|
|
24
36
|
- [Quickstart — wiring the app providers](#quickstart--wiring-the-app-providers)
|
|
25
37
|
- [Recipes](#recipes)
|
|
@@ -75,7 +87,31 @@ The goal is to start every new React frontend with the same opinionated foundati
|
|
|
75
87
|
- Fast HMR — provider files (`ThemeProvider`, `I18nProvider`, etc.) opt into React Refresh.
|
|
76
88
|
- First-class compatibility with the Vite plugin ecosystem (`vite-plugin-pwa` for service workers, `vite-plugin-dts`, `vite-plugin-svgr`, etc.).
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
**Fastest path — scaffold a fully wired app** with the `create-tempest-app` CLI that ships **inside the SDK** (Vite `@` alias, declarative routing, Zustand store, TanStack Query, providers — all pre-fiados):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# brand-new project (no install needed)
|
|
94
|
+
npx -p tempest-react-sdk create-tempest-app my-app
|
|
95
|
+
cd my-app
|
|
96
|
+
npm install
|
|
97
|
+
npm run dev
|
|
98
|
+
|
|
99
|
+
# want it installable + web-push ready? add --pwa
|
|
100
|
+
npx -p tempest-react-sdk create-tempest-app my-app --pwa
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The `--pwa` flag overlays a manifest, a service worker built from `tempest-react-sdk/sw`, and push wiring (`usePushSubscription` + `useBeforeInstallPrompt`) on top of the base app — no `vite-plugin-pwa`. See [Scaffold › PWA mode](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/#modo-pwa-pwa).
|
|
104
|
+
|
|
105
|
+
Already have a project? Install the SDK, then scaffold `src/` + configs into it:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install tempest-react-sdk
|
|
109
|
+
npx create-tempest-app . # merges into the current dir, skips existing files
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [Scaffold a new app](#scaffold-a-new-app) for the generated layout.
|
|
113
|
+
|
|
114
|
+
Or start from a bare Vite template and add the SDK manually:
|
|
79
115
|
|
|
80
116
|
```bash
|
|
81
117
|
npm create vite@latest my-app -- --template react-ts
|
|
@@ -113,17 +149,19 @@ Requires React `>=18` and Node `>=20.19` to build.
|
|
|
113
149
|
|
|
114
150
|
Only **react** and **react-dom** are peer dependencies — those must come from the host app so a single React copy lives in the tree.
|
|
115
151
|
|
|
116
|
-
Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `lucide-react`) is a **direct dependency** of the SDK, installed automatically by `npm install tempest-react-sdk`. You never need to install them manually.
|
|
152
|
+
Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `react-router-dom`, `lucide-react`) is a **direct dependency** of the SDK, installed automatically by `npm install tempest-react-sdk`. You never need to install them manually.
|
|
117
153
|
|
|
118
|
-
| Package | Status | Used by
|
|
119
|
-
| ------------------------------------- | ------------------- |
|
|
120
|
-
| `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything
|
|
121
|
-
| `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys`
|
|
122
|
-
| `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm`
|
|
123
|
-
| `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore`
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
154
|
+
| Package | Status | Used by |
|
|
155
|
+
| ------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
|
|
156
|
+
| `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything |
|
|
157
|
+
| `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys`, `AppProviders` |
|
|
158
|
+
| `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm` |
|
|
159
|
+
| `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore`, `createStore`, `createSelectors` |
|
|
160
|
+
| `react-router-dom` (`^7`) | Direct dep (auto) | `AppRouter`, `defineRoutes`, `RouteGuard`, routing re-exports |
|
|
161
|
+
| `dexie` (`^4.4`) | Direct dep (auto) | `createOfflineStore` |
|
|
162
|
+
| `react-hook-form` (`^7.76`) | Direct dep (auto) | `zodResolver`, `useZodForm`, masked inputs |
|
|
163
|
+
| `lucide-react` (`>=0.400`) | Direct dep (auto) | Component icons (`leftIcon`/`rightIcon` on `Input`, `Button`, etc.) |
|
|
164
|
+
| `vite`, `@vitejs/plugin-react` | **Optional peer** | `createViteConfig` (`tempest-react-sdk/vite`) — already in any Vite app |
|
|
127
165
|
|
|
128
166
|
The minimum install is just:
|
|
129
167
|
|
|
@@ -155,35 +193,168 @@ The styles ship hashed under the `tempest_` namespace — they do **not** collid
|
|
|
155
193
|
|
|
156
194
|
Every module is re-exported from the package root — `import { Button, useDebounce, createApiClient } from "tempest-react-sdk"` always works.
|
|
157
195
|
|
|
158
|
-
| Module
|
|
159
|
-
|
|
|
160
|
-
| `components`
|
|
161
|
-
| `hooks`
|
|
162
|
-
| `http`
|
|
163
|
-
| `auth` _(peer: `zustand`)_
|
|
164
|
-
| `query` _(peer: `@tanstack/react-query`)_
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
| Module | Exports |
|
|
197
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
198
|
+
| `components` | `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
|
|
199
|
+
| `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useDeepMemo` |
|
|
200
|
+
| `http` | `createApiClient`, `parseResponse`, `uploadWithProgress`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
|
|
201
|
+
| `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
|
|
202
|
+
| `query` _(peer: `@tanstack/react-query`)_ | `QueryProvider`, `createQueryKeys`, `STALE_TIME`, `CACHE_TIME`, `REFETCH_TIME` |
|
|
203
|
+
| `router` _(dep: `react-router-dom`)_ | `defineRoutes`, `AppRouter`, `RouteGuard`, + re-exports (`Link`, `NavLink`, `Outlet`, `Navigate`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`, `useMatch`, `useRouteError`, `redirect`, `BrowserRouter`/`HashRouter`/`MemoryRouter`/`Routes`/`Route`), types: `TempestRouteObject`, `RouterKind`, `AppRouterProps`, `RouteGuardProps` |
|
|
204
|
+
| `store` _(dep: `zustand`)_ | `createStore`, `createSelectors`, types: `CreateStoreOptions`, `CreateStorePersistOptions`, `WithSelectors` |
|
|
205
|
+
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
206
|
+
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig` |
|
|
207
|
+
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
208
|
+
| `sse` | `createEventStream`, `useEventStream` |
|
|
209
|
+
| `ws` | `createWebSocket`, `useWebSocket` |
|
|
210
|
+
| `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
|
|
211
|
+
| `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker`, `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener` — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
|
|
212
|
+
| `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio` |
|
|
213
|
+
| `offline` _(peer: `dexie`)_ | `createOfflineStore`, types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions` |
|
|
214
|
+
| `error-boundary` | `ErrorBoundary`, `useErrorHandler`, types: `ErrorBoundaryProps`, `ErrorBoundaryRenderProps` |
|
|
215
|
+
| `theme` | `ThemeProvider`, `useTheme`, `getInitialTheme`, `themeInitScript`, types: `ThemeMode`, `ResolvedTheme` |
|
|
216
|
+
| `i18n` | `createI18n`, `I18nProvider`, `useI18n`, `useTranslate`, types: `Catalog`, `Messages`, `I18n`, `InterpolationValues` |
|
|
217
|
+
| `logger` | `createLogger`, `consoleSink`, types: `Logger`, `LogEntry`, `LogLevel`, `LoggerSink` |
|
|
218
|
+
| `telemetry` | `TelemetryProvider`, `useTelemetry`, `consoleTelemetryAdapter`, `createSentryTelemetryAdapter`, `createPostHogTelemetryAdapter`, types: `TelemetryAdapter`, `TelemetryEvent`, `TelemetryUser`, `CreateSentryTelemetryAdapterOptions`, `SentryLike`, `CreatePostHogTelemetryAdapterOptions`, `PostHogLike` |
|
|
219
|
+
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
220
|
+
| `share` | `share`, `isShareSupported`, types: `SharePayload`, `ShareResult` |
|
|
221
|
+
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime` |
|
|
222
|
+
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
223
|
+
|
|
224
|
+
Full per-module docs are published as a bilingual MkDocs site on GitHub Pages — **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** / **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** (one page per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)). The source markdown lives in [`docs/`](./docs) (PT-BR base files + `.en.md` translations).
|
|
225
|
+
|
|
226
|
+
> **Local preview:** `pip install -r docs/requirements.txt && mkdocs serve` (the published site is built and deployed automatically by `.github/workflows/docs.yml`).
|
|
182
227
|
|
|
183
228
|
A demo app exercising every module lives in [`examples/gallery`](./examples/gallery) — `cd examples/gallery && npm install && npm run dev`.
|
|
184
229
|
|
|
185
230
|
---
|
|
186
231
|
|
|
232
|
+
## Scaffold a new app
|
|
233
|
+
|
|
234
|
+
The **`create-tempest-app`** CLI **ships inside the `tempest-react-sdk` package** (it is the package's `bin`) and generates a ready-to-run Vite + React 19 + TypeScript project already wired with the SDK — no manual provider/router/store setup:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# brand-new project folder (npx pulls the SDK and runs its bin)
|
|
238
|
+
npx -p tempest-react-sdk create-tempest-app my-app
|
|
239
|
+
cd my-app
|
|
240
|
+
npm install
|
|
241
|
+
cp .env.example .env
|
|
242
|
+
npm run dev # http://127.0.0.1:5173
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or, in a project that already depends on the SDK, scaffold into the current directory (existing files are left untouched; an existing `package.json` gets the Tempest scripts/deps merged in):
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm install tempest-react-sdk
|
|
249
|
+
npx create-tempest-app .
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Generated layout:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
my-app/
|
|
256
|
+
├── index.html
|
|
257
|
+
├── package.json # react, react-dom, tempest-react-sdk (+ vite/ts devDeps)
|
|
258
|
+
├── tsconfig.json # "@/*" -> "./src/*"
|
|
259
|
+
├── vite.config.ts # export default createViteConfig()
|
|
260
|
+
├── .env.example # VITE_API_URL
|
|
261
|
+
└── src/
|
|
262
|
+
├── main.tsx # createRoot + "tempest-react-sdk/styles.css" + <App/>
|
|
263
|
+
├── App.tsx # <AppProviders> → <AppRouter routes fallback/>
|
|
264
|
+
├── routes.tsx # defineRoutes([...]) — index, login, lazy + guarded dashboard
|
|
265
|
+
├── layouts/RootLayout.tsx # nav (Link) + <Outlet/>
|
|
266
|
+
├── pages/ # Home, Login, Dashboard (lazy + protected)
|
|
267
|
+
├── stores/auth.ts # createSelectors(createAuthStore<User>())
|
|
268
|
+
└── lib/api.ts # createApiClient(...) + createQueryKeys
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Each generated file demonstrates one SDK capability. Full walkthrough: **[scaffold docs (PT)](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/)** · **[EN](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/scaffold/)**.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## App foundation
|
|
276
|
+
|
|
277
|
+
Beyond UI blocks, the SDK ships an opinionated **application foundation** so every Tempest frontend wires Vite, routing, state and cache the same way. These are also what the scaffold above generates.
|
|
278
|
+
|
|
279
|
+
**Vite config** — one call wires `@vitejs/plugin-react`, the `@` → `src` alias and dev-server defaults (import from the Node subpath):
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// vite.config.ts
|
|
283
|
+
import { createViteConfig } from "tempest-react-sdk/vite";
|
|
284
|
+
|
|
285
|
+
export default createViteConfig({
|
|
286
|
+
proxy: { "/api": "http://127.0.0.1:8000" },
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> Declare the same alias in `tsconfig.json` so the type-checker resolves it: `"paths": { "@/*": ["./src/*"] }`.
|
|
291
|
+
|
|
292
|
+
**Declarative routing** (React Router v7) — describe the tree as data, with `lazy` code-splitting and per-route `guard` redirects:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import { defineRoutes, AppRouter } from "tempest-react-sdk";
|
|
296
|
+
import { useAuth } from "@/stores/auth";
|
|
297
|
+
|
|
298
|
+
export const routes = defineRoutes([
|
|
299
|
+
{
|
|
300
|
+
path: "/",
|
|
301
|
+
element: <RootLayout />,
|
|
302
|
+
children: [
|
|
303
|
+
{ index: true, element: <Home /> },
|
|
304
|
+
{ path: "login", element: <Login /> },
|
|
305
|
+
{
|
|
306
|
+
path: "dashboard",
|
|
307
|
+
lazy: () => import("@/pages/Dashboard"),
|
|
308
|
+
guard: () => useAuth.getState().isAuthenticated,
|
|
309
|
+
redirectTo: "/login",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
// <AppRouter routes={routes} fallback={<p>Loading…</p>} />
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`AppRouter` also re-exports `Link`, `NavLink`, `Outlet`, `Navigate`, `useNavigate`, `useParams`, … so apps import their whole routing surface from the SDK.
|
|
319
|
+
|
|
320
|
+
**State (Zustand)** — `createStore` for any domain slice, `createSelectors` for per-field subscription hooks:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { createStore, createSelectors } from "tempest-react-sdk";
|
|
324
|
+
|
|
325
|
+
interface CartState {
|
|
326
|
+
items: string[];
|
|
327
|
+
add: (id: string) => void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const useCart = createSelectors(
|
|
331
|
+
createStore<CartState>(
|
|
332
|
+
(set) => ({ items: [], add: (id) => set((s) => ({ items: [...s.items, id] })) }),
|
|
333
|
+
{ persist: { name: "cart", partialize: (s) => ({ items: s.items }) } },
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
// const items = useCart.use.items(); // subscribes only to `items`
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Provider composition** — `AppProviders` nests ErrorBoundary → Query → Theme → i18n in one block (Query + Theme on by default; i18n + ErrorBoundary opt-in):
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { AppProviders, AppRouter } from "tempest-react-sdk";
|
|
343
|
+
import { routes } from "@/routes";
|
|
344
|
+
|
|
345
|
+
export function App() {
|
|
346
|
+
return (
|
|
347
|
+
<AppProviders errorBoundary={{ fallback: <p>Something went wrong.</p> }}>
|
|
348
|
+
<AppRouter routes={routes} fallback={<p>Loading…</p>} />
|
|
349
|
+
</AppProviders>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Per-topic guides (bilingual): **[Routing](https://mauriciobenjamin700.github.io/tempest-react-sdk/routing/)** · **[State](https://mauriciobenjamin700.github.io/tempest-react-sdk/state/)** · **[Providers](https://mauriciobenjamin700.github.io/tempest-react-sdk/app-providers/)** · **[Vite & alias](https://mauriciobenjamin700.github.io/tempest-react-sdk/vite-config/)**.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
187
358
|
## Architecture overview
|
|
188
359
|
|
|
189
360
|
The SDK is a layered set of building blocks. Apps wire the layers together; the SDK never owns the app shell.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-tempest-app — ships inside tempest-react-sdk.
|
|
3
|
+
//
|
|
4
|
+
// npx create-tempest-app my-app → scaffold a brand-new project folder
|
|
5
|
+
// npx create-tempest-app . → scaffold into the current directory
|
|
6
|
+
// npx create-tempest-app → same as "." (merge into the current dir)
|
|
7
|
+
// npx create-tempest-app my-app --pwa → also wire installability + web-push
|
|
8
|
+
//
|
|
9
|
+
// In merge mode, files the user already has are left untouched and an existing
|
|
10
|
+
// package.json has the Tempest scripts/deps merged in (your name/version are
|
|
11
|
+
// preserved). The `--pwa` flag overlays the PWA template (manifest, service
|
|
12
|
+
// worker, push-subscribe wiring) on top of the base.
|
|
13
|
+
import { cp, mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const PKG_ROOT = resolve(__dirname, "..");
|
|
20
|
+
const TEMPLATE_DIR = join(PKG_ROOT, "template");
|
|
21
|
+
const TEMPLATE_PWA_DIR = join(PKG_ROOT, "template-pwa");
|
|
22
|
+
|
|
23
|
+
/** Files renamed on copy so they ship inside the npm tarball. */
|
|
24
|
+
const RENAME_ON_COPY = {
|
|
25
|
+
_gitignore: ".gitignore",
|
|
26
|
+
"_env.example": ".env.example",
|
|
27
|
+
"_prettierrc.json": ".prettierrc.json",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const c = {
|
|
31
|
+
reset: "\x1b[0m",
|
|
32
|
+
bold: "\x1b[1m",
|
|
33
|
+
dim: "\x1b[2m",
|
|
34
|
+
green: "\x1b[32m",
|
|
35
|
+
cyan: "\x1b[36m",
|
|
36
|
+
yellow: "\x1b[33m",
|
|
37
|
+
red: "\x1b[31m",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function isValidName(name) {
|
|
41
|
+
return /^[a-z0-9._-]+$/i.test(name) && !name.startsWith(".");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Final on-disk name for a template entry (dotfiles are unprefixed on copy). */
|
|
45
|
+
function finalName(entryName) {
|
|
46
|
+
return RENAME_ON_COPY[entryName] ?? entryName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recursively collect existing file paths under `dir`, relative to it. */
|
|
50
|
+
async function listFiles(dir, relBase = "", out = new Set()) {
|
|
51
|
+
if (!existsSync(dir)) return out;
|
|
52
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const rel = join(relBase, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
await listFiles(join(dir, entry.name), rel, out);
|
|
57
|
+
} else {
|
|
58
|
+
out.add(rel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function isEmptyDir(dir) {
|
|
65
|
+
if (!existsSync(dir)) return true;
|
|
66
|
+
const entries = await readdir(dir);
|
|
67
|
+
return entries.filter((e) => e !== ".git").length === 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read the SDK's own version so the generated app pins a matching range. */
|
|
71
|
+
async function readSdkVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8"));
|
|
74
|
+
return pkg.version ?? "latest";
|
|
75
|
+
} catch {
|
|
76
|
+
return "latest";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Copy a template tree into `destDir`. `package.json` is handled separately;
|
|
82
|
+
* any path already present in `protect` (the user's own files) is skipped and
|
|
83
|
+
* recorded. Everything else is overwritten, so a later overlay wins over the
|
|
84
|
+
* base it sits on.
|
|
85
|
+
*/
|
|
86
|
+
async function copyTree(srcDir, destDir, protect, skipped, relBase = "") {
|
|
87
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const rel = join(relBase, finalName(entry.name));
|
|
90
|
+
const from = join(srcDir, entry.name);
|
|
91
|
+
const to = join(destDir, finalName(entry.name));
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
await mkdir(to, { recursive: true });
|
|
94
|
+
await copyTree(from, to, protect, skipped, rel);
|
|
95
|
+
} else if (entry.name === "package.json") {
|
|
96
|
+
// handled separately by writeFreshPackageJson / mergePackageJson
|
|
97
|
+
} else if (protect.has(rel)) {
|
|
98
|
+
skipped.add(rel);
|
|
99
|
+
} else {
|
|
100
|
+
await cp(from, to);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Recursively rename underscore-prefixed dotfiles after a directory copy. */
|
|
106
|
+
async function fixDotfiles(dir) {
|
|
107
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const full = join(dir, entry.name);
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
await fixDotfiles(full);
|
|
112
|
+
} else if (RENAME_ON_COPY[entry.name]) {
|
|
113
|
+
await rename(full, join(dir, RENAME_ON_COPY[entry.name]));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Stamp the SDK version into a fresh package.json. */
|
|
119
|
+
async function writeFreshPackageJson(destDir, name, sdkVersion) {
|
|
120
|
+
const pkg = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
|
|
121
|
+
pkg.name = name;
|
|
122
|
+
pkg.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
|
|
123
|
+
await writeFile(join(destDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Merge Tempest scripts + deps into an existing package.json (non-destructive). */
|
|
127
|
+
async function mergePackageJson(destDir, sdkVersion) {
|
|
128
|
+
const tpl = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
|
|
129
|
+
const target = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
|
|
130
|
+
|
|
131
|
+
target.type ??= "module";
|
|
132
|
+
target.scripts = { ...tpl.scripts, ...(target.scripts ?? {}) };
|
|
133
|
+
target.dependencies = { ...(target.dependencies ?? {}), ...tpl.dependencies };
|
|
134
|
+
target.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
|
|
135
|
+
target.devDependencies = { ...(target.devDependencies ?? {}), ...tpl.devDependencies };
|
|
136
|
+
|
|
137
|
+
await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fold the PWA package.json patch into the just-written one: PWA scripts win
|
|
142
|
+
* (e.g. `build` also bundles the service worker), deps/devDeps are added.
|
|
143
|
+
*/
|
|
144
|
+
async function applyPwaPackageJson(destDir) {
|
|
145
|
+
const patch = JSON.parse(await readFile(join(TEMPLATE_PWA_DIR, "package.json"), "utf8"));
|
|
146
|
+
const target = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
|
|
147
|
+
|
|
148
|
+
target.scripts = { ...(target.scripts ?? {}), ...(patch.scripts ?? {}) };
|
|
149
|
+
target.dependencies = { ...(target.dependencies ?? {}), ...(patch.dependencies ?? {}) };
|
|
150
|
+
target.devDependencies = {
|
|
151
|
+
...(target.devDependencies ?? {}),
|
|
152
|
+
...(patch.devDependencies ?? {}),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseArgs(argv) {
|
|
159
|
+
const rest = argv.slice(2);
|
|
160
|
+
const flags = new Set(rest.filter((a) => a.startsWith("--")));
|
|
161
|
+
const positionals = rest.filter((a) => !a.startsWith("--"));
|
|
162
|
+
return { name: positionals[0], pwa: flags.has("--pwa") };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
console.log(`\n${c.bold}${c.cyan}create-tempest-app${c.reset}\n`);
|
|
167
|
+
|
|
168
|
+
if (!existsSync(TEMPLATE_DIR)) {
|
|
169
|
+
console.error(`${c.red}✗ Template not found at ${TEMPLATE_DIR}${c.reset}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { name: arg, pwa } = parseArgs(process.argv);
|
|
174
|
+
|
|
175
|
+
if (pwa && !existsSync(TEMPLATE_PWA_DIR)) {
|
|
176
|
+
console.error(`${c.red}✗ PWA template not found at ${TEMPLATE_PWA_DIR}${c.reset}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sdkVersion = await readSdkVersion();
|
|
181
|
+
const mergeMode = arg === "." || arg === undefined;
|
|
182
|
+
|
|
183
|
+
// Resolve destination + validate.
|
|
184
|
+
let destDir;
|
|
185
|
+
if (mergeMode) {
|
|
186
|
+
destDir = process.cwd();
|
|
187
|
+
console.log(`${c.dim}Scaffolding into the current directory…${c.reset}`);
|
|
188
|
+
} else {
|
|
189
|
+
if (!isValidName(arg)) {
|
|
190
|
+
console.error(`${c.red}✗ Invalid project name: "${arg}"${c.reset}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
destDir = resolve(process.cwd(), arg);
|
|
194
|
+
if (!(await isEmptyDir(destDir))) {
|
|
195
|
+
console.error(`${c.red}✗ Directory "${arg}" exists and is not empty.${c.reset}`);
|
|
196
|
+
console.error(
|
|
197
|
+
`${c.dim} Use "create-tempest-app ." to merge into the current directory.${c.reset}`,
|
|
198
|
+
);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
await mkdir(destDir, { recursive: true });
|
|
202
|
+
console.log(`${c.dim}Scaffolding into ${destDir}…${c.reset}`);
|
|
203
|
+
}
|
|
204
|
+
if (pwa) console.log(`${c.dim}PWA mode: installability + web-push wiring.${c.reset}`);
|
|
205
|
+
|
|
206
|
+
// Snapshot the user's own files so neither copy pass clobbers them.
|
|
207
|
+
const protect = await listFiles(destDir);
|
|
208
|
+
const hadPkg = protect.has("package.json");
|
|
209
|
+
const skipped = new Set();
|
|
210
|
+
|
|
211
|
+
await copyTree(TEMPLATE_DIR, destDir, protect, skipped);
|
|
212
|
+
if (pwa) await copyTree(TEMPLATE_PWA_DIR, destDir, protect, skipped);
|
|
213
|
+
await fixDotfiles(destDir);
|
|
214
|
+
|
|
215
|
+
// package.json: merge into the user's if present, otherwise write fresh.
|
|
216
|
+
if (hadPkg) {
|
|
217
|
+
await mergePackageJson(destDir, sdkVersion);
|
|
218
|
+
console.log(`${c.dim}Merged scripts + deps into existing package.json.${c.reset}`);
|
|
219
|
+
} else {
|
|
220
|
+
const name = mergeMode ? (destDir.split("/").pop() || "tempest-app").toLowerCase() : arg;
|
|
221
|
+
await writeFreshPackageJson(destDir, name, sdkVersion);
|
|
222
|
+
}
|
|
223
|
+
if (pwa) await applyPwaPackageJson(destDir);
|
|
224
|
+
|
|
225
|
+
if (skipped.size) {
|
|
226
|
+
console.log(`\n${c.yellow}Skipped ${skipped.size} existing file(s):${c.reset}`);
|
|
227
|
+
for (const f of skipped) console.log(` ${c.dim}· ${f}${c.reset}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`\n${c.green}✓ Done!${c.reset} Next steps:\n`);
|
|
231
|
+
if (!mergeMode) console.log(` ${c.bold}cd ${arg}${c.reset}`);
|
|
232
|
+
console.log(` ${c.bold}npm install${c.reset}`);
|
|
233
|
+
console.log(` ${c.bold}npm run dev${c.reset}`);
|
|
234
|
+
if (pwa) {
|
|
235
|
+
console.log(
|
|
236
|
+
`\n${c.dim}PWA: set VITE_VAPID_PUBLIC_KEY in .env, then test installability${c.reset}`,
|
|
237
|
+
);
|
|
238
|
+
console.log(
|
|
239
|
+
`${c.dim}and push with a production build: ${c.reset}${c.bold}npm run build && npm run preview${c.reset}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
console.log();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main().catch((err) => {
|
|
246
|
+
console.error(`${c.red}✗ ${err instanceof Error ? err.message : String(err)}${c.reset}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|