tempest-react-sdk 0.5.0 → 0.7.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 +193 -35
- package/bin/create-tempest-app.mjs +177 -0
- package/dist/styles.css +1 -1
- 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 +4531 -2950
- 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 +26 -2
- package/template/README.md +37 -0
- package/template/_env.example +2 -0
- package/template/_gitignore +9 -0
- package/template/eslint.config.js +27 -0
- package/template/index.html +12 -0
- package/template/package.json +32 -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/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://bundlephobia.com/package/tempest-react-sdk)
|
|
9
9
|
|
|
10
|
+
> 📚 **Documentation (bilingual / bilíngue):** **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** · **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** — full docs site (MkDocs Material on GitHub Pages) with a PT-BR / EN-US language switcher in the header. The site is the navigable, per-module source of truth; this README stays the npm/GitHub landing page.
|
|
11
|
+
|
|
10
12
|
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
13
|
|
|
12
14
|
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 +22,8 @@ The goal is to start every new React frontend with the same opinionated foundati
|
|
|
20
22
|
- [Peer & bundled dependencies](#peer--bundled-dependencies)
|
|
21
23
|
- [CSS import](#css-import)
|
|
22
24
|
- [What's inside](#whats-inside)
|
|
25
|
+
- [Scaffold a new app](#scaffold-a-new-app)
|
|
26
|
+
- [App foundation (routing, state, providers, Vite)](#app-foundation)
|
|
23
27
|
- [Architecture overview](#architecture-overview)
|
|
24
28
|
- [Quickstart — wiring the app providers](#quickstart--wiring-the-app-providers)
|
|
25
29
|
- [Recipes](#recipes)
|
|
@@ -75,7 +79,26 @@ The goal is to start every new React frontend with the same opinionated foundati
|
|
|
75
79
|
- Fast HMR — provider files (`ThemeProvider`, `I18nProvider`, etc.) opt into React Refresh.
|
|
76
80
|
- First-class compatibility with the Vite plugin ecosystem (`vite-plugin-pwa` for service workers, `vite-plugin-dts`, `vite-plugin-svgr`, etc.).
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
**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):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# brand-new project (no install needed)
|
|
86
|
+
npx -p tempest-react-sdk create-tempest-app my-app
|
|
87
|
+
cd my-app
|
|
88
|
+
npm install
|
|
89
|
+
npm run dev
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Already have a project? Install the SDK, then scaffold `src/` + configs into it:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install tempest-react-sdk
|
|
96
|
+
npx create-tempest-app . # merges into the current dir, skips existing files
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [Scaffold a new app](#scaffold-a-new-app) for the generated layout.
|
|
100
|
+
|
|
101
|
+
Or start from a bare Vite template and add the SDK manually:
|
|
79
102
|
|
|
80
103
|
```bash
|
|
81
104
|
npm create vite@latest my-app -- --template react-ts
|
|
@@ -113,17 +136,19 @@ Requires React `>=18` and Node `>=20.19` to build.
|
|
|
113
136
|
|
|
114
137
|
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
138
|
|
|
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.
|
|
139
|
+
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
140
|
|
|
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
|
-
| `
|
|
141
|
+
| Package | Status | Used by |
|
|
142
|
+
| ------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
|
|
143
|
+
| `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything |
|
|
144
|
+
| `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys`, `AppProviders` |
|
|
145
|
+
| `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm` |
|
|
146
|
+
| `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore`, `createStore`, `createSelectors` |
|
|
147
|
+
| `react-router-dom` (`^7`) | Direct dep (auto) | `AppRouter`, `defineRoutes`, `RouteGuard`, routing re-exports |
|
|
148
|
+
| `dexie` (`^4.4`) | Direct dep (auto) | `createOfflineStore` |
|
|
149
|
+
| `react-hook-form` (`^7.76`) | Direct dep (auto) | `zodResolver`, `useZodForm`, masked inputs |
|
|
150
|
+
| `lucide-react` (`>=0.400`) | Direct dep (auto) | Component icons (`leftIcon`/`rightIcon` on `Input`, `Button`, etc.) |
|
|
151
|
+
| `vite`, `@vitejs/plugin-react` | **Optional peer** | `createViteConfig` (`tempest-react-sdk/vite`) — already in any Vite app |
|
|
127
152
|
|
|
128
153
|
The minimum install is just:
|
|
129
154
|
|
|
@@ -155,35 +180,168 @@ The styles ship hashed under the `tempest_` namespace — they do **not** collid
|
|
|
155
180
|
|
|
156
181
|
Every module is re-exported from the package root — `import { Button, useDebounce, createApiClient } from "tempest-react-sdk"` always works.
|
|
157
182
|
|
|
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
|
-
|
|
183
|
+
| Module | Exports |
|
|
184
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
185
|
+
| `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` |
|
|
186
|
+
| `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useDeepMemo` |
|
|
187
|
+
| `http` | `createApiClient`, `parseResponse`, `uploadWithProgress`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
|
|
188
|
+
| `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
|
|
189
|
+
| `query` _(peer: `@tanstack/react-query`)_ | `QueryProvider`, `createQueryKeys`, `STALE_TIME`, `CACHE_TIME`, `REFETCH_TIME` |
|
|
190
|
+
| `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` |
|
|
191
|
+
| `store` _(dep: `zustand`)_ | `createStore`, `createSelectors`, types: `CreateStoreOptions`, `CreateStorePersistOptions`, `WithSelectors` |
|
|
192
|
+
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
193
|
+
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig` |
|
|
194
|
+
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
195
|
+
| `sse` | `createEventStream`, `useEventStream` |
|
|
196
|
+
| `ws` | `createWebSocket`, `useWebSocket` |
|
|
197
|
+
| `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
|
|
198
|
+
| `sw` | `registerServiceWorker`, `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener` |
|
|
199
|
+
| `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio` |
|
|
200
|
+
| `offline` _(peer: `dexie`)_ | `createOfflineStore`, types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions` |
|
|
201
|
+
| `error-boundary` | `ErrorBoundary`, `useErrorHandler`, types: `ErrorBoundaryProps`, `ErrorBoundaryRenderProps` |
|
|
202
|
+
| `theme` | `ThemeProvider`, `useTheme`, `getInitialTheme`, `themeInitScript`, types: `ThemeMode`, `ResolvedTheme` |
|
|
203
|
+
| `i18n` | `createI18n`, `I18nProvider`, `useI18n`, `useTranslate`, types: `Catalog`, `Messages`, `I18n`, `InterpolationValues` |
|
|
204
|
+
| `logger` | `createLogger`, `consoleSink`, types: `Logger`, `LogEntry`, `LogLevel`, `LoggerSink` |
|
|
205
|
+
| `telemetry` | `TelemetryProvider`, `useTelemetry`, `consoleTelemetryAdapter`, `createSentryTelemetryAdapter`, `createPostHogTelemetryAdapter`, types: `TelemetryAdapter`, `TelemetryEvent`, `TelemetryUser`, `CreateSentryTelemetryAdapterOptions`, `SentryLike`, `CreatePostHogTelemetryAdapterOptions`, `PostHogLike` |
|
|
206
|
+
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
207
|
+
| `share` | `share`, `isShareSupported`, types: `SharePayload`, `ShareResult` |
|
|
208
|
+
| `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` |
|
|
209
|
+
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
210
|
+
|
|
211
|
+
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).
|
|
212
|
+
|
|
213
|
+
> **Local preview:** `pip install -r docs/requirements.txt && mkdocs serve` (the published site is built and deployed automatically by `.github/workflows/docs.yml`).
|
|
182
214
|
|
|
183
215
|
A demo app exercising every module lives in [`examples/gallery`](./examples/gallery) — `cd examples/gallery && npm install && npm run dev`.
|
|
184
216
|
|
|
185
217
|
---
|
|
186
218
|
|
|
219
|
+
## Scaffold a new app
|
|
220
|
+
|
|
221
|
+
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:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# brand-new project folder (npx pulls the SDK and runs its bin)
|
|
225
|
+
npx -p tempest-react-sdk create-tempest-app my-app
|
|
226
|
+
cd my-app
|
|
227
|
+
npm install
|
|
228
|
+
cp .env.example .env
|
|
229
|
+
npm run dev # http://127.0.0.1:5173
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
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):
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm install tempest-react-sdk
|
|
236
|
+
npx create-tempest-app .
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Generated layout:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
my-app/
|
|
243
|
+
├── index.html
|
|
244
|
+
├── package.json # react, react-dom, tempest-react-sdk (+ vite/ts devDeps)
|
|
245
|
+
├── tsconfig.json # "@/*" -> "./src/*"
|
|
246
|
+
├── vite.config.ts # export default createViteConfig()
|
|
247
|
+
├── .env.example # VITE_API_URL
|
|
248
|
+
└── src/
|
|
249
|
+
├── main.tsx # createRoot + "tempest-react-sdk/styles.css" + <App/>
|
|
250
|
+
├── App.tsx # <AppProviders> → <AppRouter routes fallback/>
|
|
251
|
+
├── routes.tsx # defineRoutes([...]) — index, login, lazy + guarded dashboard
|
|
252
|
+
├── layouts/RootLayout.tsx # nav (Link) + <Outlet/>
|
|
253
|
+
├── pages/ # Home, Login, Dashboard (lazy + protected)
|
|
254
|
+
├── stores/auth.ts # createSelectors(createAuthStore<User>())
|
|
255
|
+
└── lib/api.ts # createApiClient(...) + createQueryKeys
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
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/)**.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## App foundation
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
**Vite config** — one call wires `@vitejs/plugin-react`, the `@` → `src` alias and dev-server defaults (import from the Node subpath):
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// vite.config.ts
|
|
270
|
+
import { createViteConfig } from "tempest-react-sdk/vite";
|
|
271
|
+
|
|
272
|
+
export default createViteConfig({
|
|
273
|
+
proxy: { "/api": "http://127.0.0.1:8000" },
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> Declare the same alias in `tsconfig.json` so the type-checker resolves it: `"paths": { "@/*": ["./src/*"] }`.
|
|
278
|
+
|
|
279
|
+
**Declarative routing** (React Router v7) — describe the tree as data, with `lazy` code-splitting and per-route `guard` redirects:
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
import { defineRoutes, AppRouter } from "tempest-react-sdk";
|
|
283
|
+
import { useAuth } from "@/stores/auth";
|
|
284
|
+
|
|
285
|
+
export const routes = defineRoutes([
|
|
286
|
+
{
|
|
287
|
+
path: "/",
|
|
288
|
+
element: <RootLayout />,
|
|
289
|
+
children: [
|
|
290
|
+
{ index: true, element: <Home /> },
|
|
291
|
+
{ path: "login", element: <Login /> },
|
|
292
|
+
{
|
|
293
|
+
path: "dashboard",
|
|
294
|
+
lazy: () => import("@/pages/Dashboard"),
|
|
295
|
+
guard: () => useAuth.getState().isAuthenticated,
|
|
296
|
+
redirectTo: "/login",
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
// <AppRouter routes={routes} fallback={<p>Loading…</p>} />
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`AppRouter` also re-exports `Link`, `NavLink`, `Outlet`, `Navigate`, `useNavigate`, `useParams`, … so apps import their whole routing surface from the SDK.
|
|
306
|
+
|
|
307
|
+
**State (Zustand)** — `createStore` for any domain slice, `createSelectors` for per-field subscription hooks:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { createStore, createSelectors } from "tempest-react-sdk";
|
|
311
|
+
|
|
312
|
+
interface CartState {
|
|
313
|
+
items: string[];
|
|
314
|
+
add: (id: string) => void;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export const useCart = createSelectors(
|
|
318
|
+
createStore<CartState>(
|
|
319
|
+
(set) => ({ items: [], add: (id) => set((s) => ({ items: [...s.items, id] })) }),
|
|
320
|
+
{ persist: { name: "cart", partialize: (s) => ({ items: s.items }) } },
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
// const items = useCart.use.items(); // subscribes only to `items`
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Provider composition** — `AppProviders` nests ErrorBoundary → Query → Theme → i18n in one block (Query + Theme on by default; i18n + ErrorBoundary opt-in):
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { AppProviders, AppRouter } from "tempest-react-sdk";
|
|
330
|
+
import { routes } from "@/routes";
|
|
331
|
+
|
|
332
|
+
export function App() {
|
|
333
|
+
return (
|
|
334
|
+
<AppProviders errorBoundary={{ fallback: <p>Something went wrong.</p> }}>
|
|
335
|
+
<AppRouter routes={routes} fallback={<p>Loading…</p>} />
|
|
336
|
+
</AppProviders>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
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/)**.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
187
345
|
## Architecture overview
|
|
188
346
|
|
|
189
347
|
The SDK is a layered set of building blocks. Apps wire the layers together; the SDK never owns the app shell.
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
//
|
|
8
|
+
// In merge mode, existing files are left untouched and an existing package.json
|
|
9
|
+
// has the Tempest scripts/deps merged in (your name/version are preserved).
|
|
10
|
+
import { cp, mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PKG_ROOT = resolve(__dirname, "..");
|
|
17
|
+
const TEMPLATE_DIR = join(PKG_ROOT, "template");
|
|
18
|
+
|
|
19
|
+
/** Files renamed on copy so they ship inside the npm tarball. */
|
|
20
|
+
const RENAME_ON_COPY = { _gitignore: ".gitignore", "_env.example": ".env.example" };
|
|
21
|
+
|
|
22
|
+
const c = {
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
bold: "\x1b[1m",
|
|
25
|
+
dim: "\x1b[2m",
|
|
26
|
+
green: "\x1b[32m",
|
|
27
|
+
cyan: "\x1b[36m",
|
|
28
|
+
yellow: "\x1b[33m",
|
|
29
|
+
red: "\x1b[31m",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function isValidName(name) {
|
|
33
|
+
return /^[a-z0-9._-]+$/i.test(name) && !name.startsWith(".");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function isEmptyDir(dir) {
|
|
37
|
+
if (!existsSync(dir)) return true;
|
|
38
|
+
const entries = await readdir(dir);
|
|
39
|
+
return entries.filter((e) => e !== ".git").length === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read the SDK's own version so the generated app pins a matching range. */
|
|
43
|
+
async function readSdkVersion() {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8"));
|
|
46
|
+
return pkg.version ?? "latest";
|
|
47
|
+
} catch {
|
|
48
|
+
return "latest";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Recursively rename underscore-prefixed dotfiles after a directory copy. */
|
|
53
|
+
async function fixDotfiles(dir) {
|
|
54
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const full = join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
await fixDotfiles(full);
|
|
59
|
+
} else if (RENAME_ON_COPY[entry.name]) {
|
|
60
|
+
await rename(full, join(dir, RENAME_ON_COPY[entry.name]));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Recursively copy template files, skipping any that already exist in dest. */
|
|
66
|
+
async function mergeCopy(srcDir, destDir, skipped, relBase = "") {
|
|
67
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const rel = join(relBase, RENAME_ON_COPY[entry.name] ?? entry.name);
|
|
70
|
+
const from = join(srcDir, entry.name);
|
|
71
|
+
const to = join(destDir, RENAME_ON_COPY[entry.name] ?? entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
await mkdir(to, { recursive: true });
|
|
74
|
+
await mergeCopy(from, to, skipped, rel);
|
|
75
|
+
} else if (entry.name === "package.json") {
|
|
76
|
+
// handled separately by mergePackageJson
|
|
77
|
+
} else if (existsSync(to)) {
|
|
78
|
+
skipped.push(rel);
|
|
79
|
+
} else {
|
|
80
|
+
await cp(from, to);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Stamp the SDK version into a fresh package.json. */
|
|
86
|
+
async function writeFreshPackageJson(destDir, name, sdkVersion) {
|
|
87
|
+
const pkg = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
|
|
88
|
+
pkg.name = name;
|
|
89
|
+
pkg.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
|
|
90
|
+
await writeFile(join(destDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Merge Tempest scripts + deps into an existing package.json (non-destructive). */
|
|
94
|
+
async function mergePackageJson(destDir, sdkVersion) {
|
|
95
|
+
const tpl = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
|
|
96
|
+
const target = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
|
|
97
|
+
|
|
98
|
+
target.type ??= "module";
|
|
99
|
+
target.scripts = { ...tpl.scripts, ...(target.scripts ?? {}) };
|
|
100
|
+
target.dependencies = { ...(target.dependencies ?? {}), ...tpl.dependencies };
|
|
101
|
+
target.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
|
|
102
|
+
target.devDependencies = { ...(target.devDependencies ?? {}), ...tpl.devDependencies };
|
|
103
|
+
|
|
104
|
+
await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
console.log(`\n${c.bold}${c.cyan}create-tempest-app${c.reset}\n`);
|
|
109
|
+
|
|
110
|
+
if (!existsSync(TEMPLATE_DIR)) {
|
|
111
|
+
console.error(`${c.red}✗ Template not found at ${TEMPLATE_DIR}${c.reset}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sdkVersion = await readSdkVersion();
|
|
116
|
+
const arg = process.argv[2];
|
|
117
|
+
|
|
118
|
+
// Merge mode: "." or no arg → scaffold into the current directory.
|
|
119
|
+
if (arg === "." || arg === undefined) {
|
|
120
|
+
const destDir = process.cwd();
|
|
121
|
+
const hasPkg = existsSync(join(destDir, "package.json"));
|
|
122
|
+
console.log(`${c.dim}Scaffolding into the current directory…${c.reset}`);
|
|
123
|
+
|
|
124
|
+
const skipped = [];
|
|
125
|
+
await mergeCopy(TEMPLATE_DIR, destDir, skipped);
|
|
126
|
+
await fixDotfiles(destDir);
|
|
127
|
+
|
|
128
|
+
if (hasPkg) {
|
|
129
|
+
await mergePackageJson(destDir, sdkVersion);
|
|
130
|
+
console.log(`${c.dim}Merged scripts + deps into existing package.json.${c.reset}`);
|
|
131
|
+
} else {
|
|
132
|
+
const name = (destDir.split("/").pop() || "tempest-app").toLowerCase();
|
|
133
|
+
await writeFreshPackageJson(destDir, name, sdkVersion);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (skipped.length) {
|
|
137
|
+
console.log(`\n${c.yellow}Skipped ${skipped.length} existing file(s):${c.reset}`);
|
|
138
|
+
for (const f of skipped) console.log(` ${c.dim}· ${f}${c.reset}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`\n${c.green}✓ Done!${c.reset} Next steps:\n`);
|
|
142
|
+
console.log(` ${c.bold}npm install${c.reset}`);
|
|
143
|
+
console.log(` ${c.bold}npm run dev${c.reset}\n`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// New-project mode: scaffold a fresh folder from the given name.
|
|
148
|
+
if (!isValidName(arg)) {
|
|
149
|
+
console.error(`${c.red}✗ Invalid project name: "${arg}"${c.reset}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const destDir = resolve(process.cwd(), arg);
|
|
154
|
+
if (!(await isEmptyDir(destDir))) {
|
|
155
|
+
console.error(`${c.red}✗ Directory "${arg}" exists and is not empty.${c.reset}`);
|
|
156
|
+
console.error(
|
|
157
|
+
`${c.dim} Use "create-tempest-app ." to merge into the current directory.${c.reset}`,
|
|
158
|
+
);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`${c.dim}Scaffolding into ${destDir}…${c.reset}`);
|
|
163
|
+
await mkdir(destDir, { recursive: true });
|
|
164
|
+
await cp(TEMPLATE_DIR, destDir, { recursive: true });
|
|
165
|
+
await fixDotfiles(destDir);
|
|
166
|
+
await writeFreshPackageJson(destDir, arg, sdkVersion);
|
|
167
|
+
|
|
168
|
+
console.log(`\n${c.green}✓ Done!${c.reset} Next steps:\n`);
|
|
169
|
+
console.log(` ${c.bold}cd ${arg}${c.reset}`);
|
|
170
|
+
console.log(` ${c.bold}npm install${c.reset}`);
|
|
171
|
+
console.log(` ${c.bold}npm run dev${c.reset}\n`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error(`${c.red}✗ ${err instanceof Error ? err.message : String(err)}${c.reset}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|