tempest-react-sdk 0.7.0 → 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 +41 -28
- package/bin/create-tempest-app.mjs +141 -70
- package/bin/tempest.mjs +282 -0
- 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 +3 -3
- package/dist/tempest-react-sdk.cjs.map +1 -1
- package/dist/tempest-react-sdk.js +1541 -1626
- package/dist/tempest-react-sdk.js.map +1 -1
- package/package.json +10 -2
- package/template/_prettierrc.json +9 -0
- package/template/eslint.config.js +18 -1
- package/template/package.json +7 -1
- 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,7 +7,15 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://bundlephobia.com/package/tempest-react-sdk)
|
|
9
9
|
|
|
10
|
-
|
|
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.
|
|
11
19
|
|
|
12
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.).
|
|
13
21
|
|
|
@@ -87,8 +95,13 @@ npx -p tempest-react-sdk create-tempest-app my-app
|
|
|
87
95
|
cd my-app
|
|
88
96
|
npm install
|
|
89
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
|
|
90
101
|
```
|
|
91
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
|
+
|
|
92
105
|
Already have a project? Install the SDK, then scaffold `src/` + configs into it:
|
|
93
106
|
|
|
94
107
|
```bash
|
|
@@ -180,33 +193,33 @@ The styles ship hashed under the `tempest_` namespace — they do **not** collid
|
|
|
180
193
|
|
|
181
194
|
Every module is re-exported from the package root — `import { Button, useDebounce, createApiClient } from "tempest-react-sdk"` always works.
|
|
182
195
|
|
|
183
|
-
| Module
|
|
184
|
-
|
|
|
185
|
-
| `components`
|
|
186
|
-
| `hooks`
|
|
187
|
-
| `http`
|
|
188
|
-
| `auth` _(peer: `zustand`)_
|
|
189
|
-
| `query` _(peer: `@tanstack/react-query`)_
|
|
190
|
-
| `router` _(dep: `react-router-dom`)_
|
|
191
|
-
| `store` _(dep: `zustand`)_
|
|
192
|
-
| `app`
|
|
193
|
-
| `vite` _(subpath `tempest-react-sdk/vite`)_
|
|
194
|
-
| `forms` _(peer: `zod`, `react-hook-form`)_
|
|
195
|
-
| `sse`
|
|
196
|
-
| `ws`
|
|
197
|
-
| `push`
|
|
198
|
-
| `sw`
|
|
199
|
-
| `audio`
|
|
200
|
-
| `offline` _(peer: `dexie`)_
|
|
201
|
-
| `error-boundary`
|
|
202
|
-
| `theme`
|
|
203
|
-
| `i18n`
|
|
204
|
-
| `logger`
|
|
205
|
-
| `telemetry`
|
|
206
|
-
| `feature-flags`
|
|
207
|
-
| `share`
|
|
208
|
-
| `utils`
|
|
209
|
-
| generic components
|
|
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`) |
|
|
210
223
|
|
|
211
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).
|
|
212
225
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// create-tempest-app — ships inside tempest-react-sdk.
|
|
3
3
|
//
|
|
4
|
-
// npx create-tempest-app my-app
|
|
5
|
-
// npx create-tempest-app .
|
|
6
|
-
// npx create-tempest-app
|
|
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
|
|
7
8
|
//
|
|
8
|
-
// In merge mode,
|
|
9
|
-
// has the Tempest scripts/deps merged in (your name/version are
|
|
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.
|
|
10
13
|
import { cp, mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
11
14
|
import { existsSync } from "node:fs";
|
|
12
15
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -15,9 +18,14 @@ import { fileURLToPath } from "node:url";
|
|
|
15
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
19
|
const PKG_ROOT = resolve(__dirname, "..");
|
|
17
20
|
const TEMPLATE_DIR = join(PKG_ROOT, "template");
|
|
21
|
+
const TEMPLATE_PWA_DIR = join(PKG_ROOT, "template-pwa");
|
|
18
22
|
|
|
19
23
|
/** Files renamed on copy so they ship inside the npm tarball. */
|
|
20
|
-
const RENAME_ON_COPY = {
|
|
24
|
+
const RENAME_ON_COPY = {
|
|
25
|
+
_gitignore: ".gitignore",
|
|
26
|
+
"_env.example": ".env.example",
|
|
27
|
+
"_prettierrc.json": ".prettierrc.json",
|
|
28
|
+
};
|
|
21
29
|
|
|
22
30
|
const c = {
|
|
23
31
|
reset: "\x1b[0m",
|
|
@@ -33,6 +41,26 @@ function isValidName(name) {
|
|
|
33
41
|
return /^[a-z0-9._-]+$/i.test(name) && !name.startsWith(".");
|
|
34
42
|
}
|
|
35
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
|
+
|
|
36
64
|
async function isEmptyDir(dir) {
|
|
37
65
|
if (!existsSync(dir)) return true;
|
|
38
66
|
const entries = await readdir(dir);
|
|
@@ -49,6 +77,31 @@ async function readSdkVersion() {
|
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
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
|
+
|
|
52
105
|
/** Recursively rename underscore-prefixed dotfiles after a directory copy. */
|
|
53
106
|
async function fixDotfiles(dir) {
|
|
54
107
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -62,26 +115,6 @@ async function fixDotfiles(dir) {
|
|
|
62
115
|
}
|
|
63
116
|
}
|
|
64
117
|
|
|
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
118
|
/** Stamp the SDK version into a fresh package.json. */
|
|
86
119
|
async function writeFreshPackageJson(destDir, name, sdkVersion) {
|
|
87
120
|
const pkg = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
|
|
@@ -104,6 +137,31 @@ async function mergePackageJson(destDir, sdkVersion) {
|
|
|
104
137
|
await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
|
|
105
138
|
}
|
|
106
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
|
+
|
|
107
165
|
async function main() {
|
|
108
166
|
console.log(`\n${c.bold}${c.cyan}create-tempest-app${c.reset}\n`);
|
|
109
167
|
|
|
@@ -112,63 +170,76 @@ async function main() {
|
|
|
112
170
|
process.exit(1);
|
|
113
171
|
}
|
|
114
172
|
|
|
115
|
-
const
|
|
116
|
-
const arg = process.argv[2];
|
|
173
|
+
const { name: arg, pwa } = parseArgs(process.argv);
|
|
117
174
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
console.log(`${c.dim}Scaffolding into the current directory…${c.reset}`);
|
|
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
|
+
}
|
|
123
179
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await fixDotfiles(destDir);
|
|
180
|
+
const sdkVersion = await readSdkVersion();
|
|
181
|
+
const mergeMode = arg === "." || arg === undefined;
|
|
127
182
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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);
|
|
134
192
|
}
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
console.
|
|
138
|
-
|
|
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);
|
|
139
200
|
}
|
|
140
|
-
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(` ${c.bold}npm install${c.reset}`);
|
|
143
|
-
console.log(` ${c.bold}npm run dev${c.reset}\n`);
|
|
144
|
-
return;
|
|
201
|
+
await mkdir(destDir, { recursive: true });
|
|
202
|
+
console.log(`${c.dim}Scaffolding into ${destDir}…${c.reset}`);
|
|
145
203
|
}
|
|
204
|
+
if (pwa) console.log(`${c.dim}PWA mode: installability + web-push wiring.${c.reset}`);
|
|
146
205
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
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();
|
|
152
210
|
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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);
|
|
160
222
|
}
|
|
223
|
+
if (pwa) await applyPwaPackageJson(destDir);
|
|
161
224
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await writeFreshPackageJson(destDir, arg, sdkVersion);
|
|
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
|
+
}
|
|
167
229
|
|
|
168
230
|
console.log(`\n${c.green}✓ Done!${c.reset} Next steps:\n`);
|
|
169
|
-
console.log(` ${c.bold}cd ${arg}${c.reset}`);
|
|
231
|
+
if (!mergeMode) console.log(` ${c.bold}cd ${arg}${c.reset}`);
|
|
170
232
|
console.log(` ${c.bold}npm install${c.reset}`);
|
|
171
|
-
console.log(` ${c.bold}npm run dev${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();
|
|
172
243
|
}
|
|
173
244
|
|
|
174
245
|
main().catch((err) => {
|