tempest-react-sdk 0.7.0 → 0.9.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 (39) hide show
  1. package/README.md +41 -28
  2. package/bin/create-tempest-app.mjs +141 -70
  3. package/bin/lib/openapi/generate.mjs +259 -0
  4. package/bin/lib/openapi/generate.test.mjs +129 -0
  5. package/bin/lib/openapi/load.mjs +24 -0
  6. package/bin/lib/openapi/schema-to-zod.mjs +123 -0
  7. package/bin/lib/openapi/schema-to-zod.test.mjs +81 -0
  8. package/bin/tempest.mjs +364 -0
  9. package/dist/sw.cjs +2 -0
  10. package/dist/sw.cjs.map +1 -0
  11. package/dist/sw.d.ts +233 -0
  12. package/dist/sw.js +361 -0
  13. package/dist/sw.js.map +1 -0
  14. package/dist/tempest-react-sdk.cjs +3 -3
  15. package/dist/tempest-react-sdk.cjs.map +1 -1
  16. package/dist/tempest-react-sdk.d.ts +130 -0
  17. package/dist/tempest-react-sdk.js +1565 -1646
  18. package/dist/tempest-react-sdk.js.map +1 -1
  19. package/dist/vite.cjs +3 -1
  20. package/dist/vite.cjs.map +1 -1
  21. package/dist/vite.d.ts +136 -0
  22. package/dist/vite.js +253 -34
  23. package/dist/vite.js.map +1 -1
  24. package/package.json +10 -2
  25. package/template/_prettierrc.json +9 -0
  26. package/template/eslint.config.js +18 -1
  27. package/template/package.json +7 -1
  28. package/template-pwa/README.md +64 -0
  29. package/template-pwa/_env.example +7 -0
  30. package/template-pwa/index.html +22 -0
  31. package/template-pwa/package.json +9 -0
  32. package/template-pwa/public/icon.svg +4 -0
  33. package/template-pwa/public/manifest.webmanifest +35 -0
  34. package/template-pwa/src/main.tsx +29 -0
  35. package/template-pwa/src/pages/Dashboard.tsx +73 -0
  36. package/template-pwa/src/sw.ts +70 -0
  37. package/template-pwa/src/vite-env.d.ts +12 -0
  38. package/template-pwa/vite.config.ts +21 -0
  39. package/template-pwa/vite.sw.config.ts +27 -0
package/dist/vite.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.js","sources":["../src/vite/create-vite-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { defineConfig } from \"vite\";\nimport type { ProxyOptions, UserConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n/**\n * A Vite proxy entry: either a target URL string (expanded to\n * `{ target, changeOrigin: true }`) or a raw Vite `ProxyOptions` object.\n */\nexport type ProxyEntry = string | Record<string, unknown>;\n\nexport interface CreateViteConfigOptions {\n /**\n * Source directory aliased to `@`, relative to the project root.\n * Default: `\"src\"` (so `@/components/Button` → `<root>/src/components/Button`).\n */\n srcDir?: string;\n /** Dev server port. Default: `5173`. */\n port?: number;\n /** Dev server host. Default: `\"127.0.0.1\"`. */\n host?: string | boolean;\n /** Open the browser on `dev` start. Default: `false`. */\n open?: boolean;\n /**\n * Dev proxy table. String values are expanded to\n * `{ target, changeOrigin: true }`; objects are passed through untouched.\n *\n * @example { \"/api\": \"http://127.0.0.1:8000\" }\n */\n proxy?: Record<string, ProxyEntry>;\n /** Extra path aliases merged on top of the default `@` → src alias. */\n alias?: Record<string, string>;\n /** Vite plugins appended after `@vitejs/plugin-react`. */\n plugins?: unknown[];\n /**\n * Arbitrary Vite config (a `UserConfig` object) deep-merged last, for\n * escape-hatch overrides (build target, define, extra `server` keys, …).\n */\n overrides?: Record<string, unknown>;\n}\n\n/**\n * The resulting Vite config object. Typed loosely so the SDK's published\n * declarations stay free of `vite`'s internal types; assign it straight to a\n * `vite.config.ts` default export.\n */\nexport type TempestViteConfig = Record<string, unknown>;\n\nfunction normalizeProxy(proxy: Record<string, ProxyEntry>): Record<string, ProxyOptions> {\n const out: Record<string, ProxyOptions> = {};\n for (const [path, value] of Object.entries(proxy)) {\n out[path] =\n typeof value === \"string\"\n ? { target: value, changeOrigin: true }\n : (value as ProxyOptions);\n }\n return out;\n}\n\n/**\n * Build a Tempest-flavored Vite config for a React app: the `@vitejs/plugin-react`\n * plugin, the `@` → `src` import alias, and sane dev-server defaults — so a\n * consuming app's `vite.config.ts` is a single call instead of repeated\n * boilerplate. Everything is overridable.\n *\n * Import it from the dedicated Node entry point:\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * proxy: { \"/api\": \"http://127.0.0.1:8000\" },\n * });\n */\nexport function createViteConfig(options: CreateViteConfigOptions = {}): TempestViteConfig {\n const {\n srcDir = \"src\",\n port = 5173,\n host = \"127.0.0.1\",\n open = false,\n proxy,\n alias = {},\n plugins = [],\n overrides = {},\n } = options;\n\n const overridesConfig = overrides as UserConfig;\n\n const base: UserConfig = {\n plugins: [react(), ...plugins] as UserConfig[\"plugins\"],\n resolve: {\n alias: {\n \"@\": resolve(process.cwd(), srcDir),\n ...alias,\n },\n },\n server: {\n port,\n host,\n open,\n ...(proxy ? { proxy: normalizeProxy(proxy) } : {}),\n },\n };\n\n const merged: UserConfig = {\n ...base,\n ...overridesConfig,\n plugins: [...(base.plugins ?? []), ...(overridesConfig.plugins ?? [])],\n resolve: { ...base.resolve, ...overridesConfig.resolve },\n server: { ...base.server, ...overridesConfig.server },\n };\n\n return defineConfig(merged) as TempestViteConfig;\n}\n"],"names":["normalizeProxy","proxy","out","path","value","createViteConfig","options","srcDir","port","host","open","alias","plugins","overrides","overridesConfig","base","react","resolve","merged","defineConfig"],"mappings":";;;AAgDA,SAASA,EAAeC,GAAiE;AACrF,QAAMC,IAAoC,CAAA;AAC1C,aAAW,CAACC,GAAMC,CAAK,KAAK,OAAO,QAAQH,CAAK;AAC5C,IAAAC,EAAIC,CAAI,IACJ,OAAOC,KAAU,WACX,EAAE,QAAQA,GAAO,cAAc,GAAA,IAC9BA;AAEf,SAAOF;AACX;AAkBO,SAASG,EAAiBC,IAAmC,IAAuB;AACvF,QAAM;AAAA,IACF,QAAAC,IAAS;AAAA,IACT,MAAAC,IAAO;AAAA,IACP,MAAAC,IAAO;AAAA,IACP,MAAAC,IAAO;AAAA,IACP,OAAAT;AAAA,IACA,OAAAU,IAAQ,CAAA;AAAA,IACR,SAAAC,IAAU,CAAA;AAAA,IACV,WAAAC,IAAY,CAAA;AAAA,EAAC,IACbP,GAEEQ,IAAkBD,GAElBE,IAAmB;AAAA,IACrB,SAAS,CAACC,KAAS,GAAGJ,CAAO;AAAA,IAC7B,SAAS;AAAA,MACL,OAAO;AAAA,QACH,KAAKK,EAAQ,QAAQ,IAAA,GAAOV,CAAM;AAAA,QAClC,GAAGI;AAAA,MAAA;AAAA,IACP;AAAA,IAEJ,QAAQ;AAAA,MACJ,MAAAH;AAAA,MACA,MAAAC;AAAA,MACA,MAAAC;AAAA,MACA,GAAIT,IAAQ,EAAE,OAAOD,EAAeC,CAAK,EAAA,IAAM,CAAA;AAAA,IAAC;AAAA,EACpD,GAGEiB,IAAqB;AAAA,IACvB,GAAGH;AAAA,IACH,GAAGD;AAAA,IACH,SAAS,CAAC,GAAIC,EAAK,WAAW,CAAA,GAAK,GAAID,EAAgB,WAAW,EAAG;AAAA,IACrE,SAAS,EAAE,GAAGC,EAAK,SAAS,GAAGD,EAAgB,QAAA;AAAA,IAC/C,QAAQ,EAAE,GAAGC,EAAK,QAAQ,GAAGD,EAAgB,OAAA;AAAA,EAAO;AAGxD,SAAOK,EAAaD,CAAM;AAC9B;"}
1
+ {"version":3,"file":"vite.js","sources":["../src/vite/create-vite-config.ts","../src/vite/tempest-pwa-manifest.ts","../src/vite/tempest-pwa-icons.ts","../src/vite/tempest-pwa-dev-sw.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { defineConfig } from \"vite\";\nimport type { ProxyOptions, UserConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n/**\n * A Vite proxy entry: either a target URL string (expanded to\n * `{ target, changeOrigin: true }`) or a raw Vite `ProxyOptions` object.\n */\nexport type ProxyEntry = string | Record<string, unknown>;\n\nexport interface CreateViteConfigOptions {\n /**\n * Source directory aliased to `@`, relative to the project root.\n * Default: `\"src\"` (so `@/components/Button` → `<root>/src/components/Button`).\n */\n srcDir?: string;\n /** Dev server port. Default: `5173`. */\n port?: number;\n /** Dev server host. Default: `\"127.0.0.1\"`. */\n host?: string | boolean;\n /** Open the browser on `dev` start. Default: `false`. */\n open?: boolean;\n /**\n * Dev proxy table. String values are expanded to\n * `{ target, changeOrigin: true }`; objects are passed through untouched.\n *\n * @example { \"/api\": \"http://127.0.0.1:8000\" }\n */\n proxy?: Record<string, ProxyEntry>;\n /** Extra path aliases merged on top of the default `@` → src alias. */\n alias?: Record<string, string>;\n /** Vite plugins appended after `@vitejs/plugin-react`. */\n plugins?: unknown[];\n /**\n * Arbitrary Vite config (a `UserConfig` object) deep-merged last, for\n * escape-hatch overrides (build target, define, extra `server` keys, …).\n */\n overrides?: Record<string, unknown>;\n}\n\n/**\n * The resulting Vite config object. Typed loosely so the SDK's published\n * declarations stay free of `vite`'s internal types; assign it straight to a\n * `vite.config.ts` default export.\n */\nexport type TempestViteConfig = Record<string, unknown>;\n\nfunction normalizeProxy(proxy: Record<string, ProxyEntry>): Record<string, ProxyOptions> {\n const out: Record<string, ProxyOptions> = {};\n for (const [path, value] of Object.entries(proxy)) {\n out[path] =\n typeof value === \"string\"\n ? { target: value, changeOrigin: true }\n : (value as ProxyOptions);\n }\n return out;\n}\n\n/**\n * Build a Tempest-flavored Vite config for a React app: the `@vitejs/plugin-react`\n * plugin, the `@` → `src` import alias, and sane dev-server defaults — so a\n * consuming app's `vite.config.ts` is a single call instead of repeated\n * boilerplate. Everything is overridable.\n *\n * Import it from the dedicated Node entry point:\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * proxy: { \"/api\": \"http://127.0.0.1:8000\" },\n * });\n */\nexport function createViteConfig(options: CreateViteConfigOptions = {}): TempestViteConfig {\n const {\n srcDir = \"src\",\n port = 5173,\n host = \"127.0.0.1\",\n open = false,\n proxy,\n alias = {},\n plugins = [],\n overrides = {},\n } = options;\n\n const overridesConfig = overrides as UserConfig;\n\n const base: UserConfig = {\n plugins: [react(), ...plugins] as UserConfig[\"plugins\"],\n resolve: {\n alias: {\n \"@\": resolve(process.cwd(), srcDir),\n ...alias,\n },\n },\n server: {\n port,\n host,\n open,\n ...(proxy ? { proxy: normalizeProxy(proxy) } : {}),\n },\n };\n\n const merged: UserConfig = {\n ...base,\n ...overridesConfig,\n plugins: [...(base.plugins ?? []), ...(overridesConfig.plugins ?? [])],\n resolve: { ...base.resolve, ...overridesConfig.resolve },\n server: { ...base.server, ...overridesConfig.server },\n };\n\n return defineConfig(merged) as TempestViteConfig;\n}\n","import type { Plugin } from \"vite\";\n\n/**\n * A Vite plugin object. Typed loosely so the SDK's published declarations stay\n * free of `vite`'s internal types (which the `.d.ts` rollup can't analyze);\n * assign the result straight into a `plugins: [...]` array.\n */\nexport type TempestVitePlugin = { name: string } & Record<string, unknown>;\n\n/** Options for {@link tempestPwaManifest}. */\nexport interface TempestPwaManifestOptions {\n /** Output file name (under the build root). Default `precache-manifest.json`. */\n fileName?: string;\n /**\n * Extra URLs to precache that Vite doesn't emit into the bundle — typically\n * `public/` assets like the web manifest and icons. Default `[]`.\n */\n additionalUrls?: string[];\n /** Emitted files matching this are skipped. Default `/\\.map$/` (source maps). */\n exclude?: RegExp;\n /** Include emitted `.html` documents (the app shell). Default `true`. */\n includeHtml?: boolean;\n /**\n * App-shell document always added to the manifest, even if Vite emits it\n * after this plugin runs. Must match `installPrecache`'s `navigateFallback`\n * so offline navigations resolve. Pass `false` to disable. Default `/index.html`.\n */\n appShell?: string | false;\n}\n\nfunction joinBase(base: string, file: string): string {\n const prefix = base.endsWith(\"/\") ? base : `${base}/`;\n return `${prefix}${file}`.replace(/([^:]\\/)\\/+/g, \"$1\");\n}\n\n/** Deterministic djb2 hash → hex. Stable across rebuilds with the same assets. */\nfunction hash(input: string): string {\n let h = 5381;\n for (let i = 0; i < input.length; i++) {\n h = ((h << 5) + h + input.charCodeAt(i)) >>> 0;\n }\n return h.toString(16);\n}\n\n/**\n * Vite build plugin that emits a `precache-manifest.json` listing every built\n * asset (plus any `additionalUrls`) as root-absolute URLs, with a content-based\n * `version`. It is the dependency-free counterpart to Workbox's `__WB_MANIFEST`:\n * `installPrecache` (from `tempest-react-sdk/sw`) reads this file at the service\n * worker's `install` event to cache the app shell for offline use.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaManifest } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaManifest({ additionalUrls: [\"/manifest.webmanifest\", \"/icon.svg\"] })],\n * });\n */\nexport function tempestPwaManifest(options: TempestPwaManifestOptions = {}): TempestVitePlugin {\n const {\n fileName = \"precache-manifest.json\",\n additionalUrls = [],\n exclude = /\\.map$/,\n includeHtml = true,\n appShell = \"/index.html\",\n } = options;\n\n let base = \"/\";\n\n const plugin: Plugin = {\n name: \"tempest-pwa-manifest\",\n apply: \"build\",\n configResolved(config) {\n base = config.base ?? \"/\";\n },\n generateBundle(_outputOptions, bundle) {\n const urls = new Set<string>(additionalUrls);\n // Vite may emit index.html after this hook, so guarantee the shell.\n if (appShell) urls.add(appShell);\n for (const file of Object.keys(bundle)) {\n if (file === fileName) continue;\n if (exclude.test(file)) continue;\n if (!includeHtml && file.endsWith(\".html\")) continue;\n urls.add(joinBase(base, file));\n }\n\n const list = [...urls].sort();\n const version = hash(list.join(\"\\n\"));\n this.emitFile({\n type: \"asset\",\n fileName,\n source: JSON.stringify({ version, urls: list }),\n });\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaIcons}. */\nexport interface TempestPwaIconsOptions {\n /** Source image (SVG or large PNG), relative to the project root. Default `public/icon.svg`. */\n source?: string;\n /** Square \"any\"-purpose icon sizes to emit. Default `[192, 512]`. */\n sizes?: number[];\n /** Square \"maskable\" icon sizes to emit (with safe-zone padding). Default `[512]`. */\n maskableSizes?: number[];\n /** Apple touch icon size, or `false` to skip. Default `180`. */\n appleTouchIcon?: number | false;\n /** Output directory for the icon set, under the build root. Default `icons`. */\n outDir?: string;\n /** Opaque background for maskable + apple icons (no transparency allowed). Default `#ffffff`. */\n background?: string;\n /** Maskable safe-zone padding as a fraction of the icon. Default `0.1` (10% each side). */\n maskablePadding?: number;\n /**\n * Generate Apple splash screens (launch images) and inject the matching\n * `<link rel=\"apple-touch-startup-image\">` tags. `true` uses a built-in set\n * of common iPhone/iPad portrait sizes; pass an array to override. Default `false`.\n */\n appleSplash?: boolean | AppleSplashSpec[];\n /** Background color for splash screens. Default: `background`. */\n splashBackground?: string;\n /** Icon size on the splash as a fraction of the shorter side. Default `0.3`. */\n splashIconScale?: number;\n}\n\n/** A single Apple splash target (CSS px + device pixel ratio). */\nexport interface AppleSplashSpec {\n /** CSS width (device-width in the media query). */\n width: number;\n /** CSS height (device-height in the media query). */\n height: number;\n /** Device pixel ratio. */\n ratio: number;\n}\n\n/** Common iPhone/iPad portrait splash sizes (CSS px @ ratio). */\nconst DEFAULT_SPLASH: AppleSplashSpec[] = [\n { width: 375, height: 667, ratio: 2 }, // iPhone SE / 8\n { width: 375, height: 812, ratio: 3 }, // iPhone X / 11 Pro\n { width: 390, height: 844, ratio: 3 }, // iPhone 12 / 13 / 14\n { width: 393, height: 852, ratio: 3 }, // iPhone 14 Pro / 15\n { width: 414, height: 896, ratio: 2 }, // iPhone XR / 11\n { width: 414, height: 896, ratio: 3 }, // iPhone XS Max / 11 Pro Max\n { width: 428, height: 926, ratio: 3 }, // iPhone 13/14 Pro Max\n { width: 430, height: 932, ratio: 3 }, // iPhone 15 Pro Max\n { width: 768, height: 1024, ratio: 2 }, // iPad\n { width: 834, height: 1194, ratio: 2 }, // iPad Pro 11\"\n { width: 1024, height: 1366, ratio: 2 }, // iPad Pro 12.9\"\n];\n\nfunction splashFileName(spec: AppleSplashSpec): string {\n return `splash/apple-splash-${spec.width * spec.ratio}x${spec.height * spec.ratio}.png`;\n}\n\nfunction splashMedia(spec: AppleSplashSpec): string {\n return (\n `(device-width: ${spec.width}px) and (device-height: ${spec.height}px) ` +\n `and (-webkit-device-pixel-ratio: ${spec.ratio}) and (orientation: portrait)`\n );\n}\n\ninterface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nfunction hexToRgb(hex: string): Rgb {\n const value = hex.replace(\"#\", \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : value;\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\n/**\n * Build plugin that rasterizes a single source image into a full PWA icon set\n * (regular + maskable + apple-touch-icon), the dependency-free counterpart to\n * `@vite-pwa/assets-generator`. Rendering uses **`sharp`**, imported lazily and\n * treated as optional: if it isn't installed the plugin logs a warning and skips\n * generation (your build still succeeds; the icons just aren't produced).\n *\n * Point your `manifest.webmanifest` icon entries at the emitted files\n * (`/icons/icon-192.png`, `/icons/icon-512.png`, `/icons/maskable-512.png`) and\n * the apple touch icon at `/apple-touch-icon.png`.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaIcons } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({\n * plugins: [tempestPwaIcons({ source: \"public/icon.svg\" })],\n * });\n */\nexport function tempestPwaIcons(options: TempestPwaIconsOptions = {}): TempestVitePlugin {\n const {\n source = \"public/icon.svg\",\n sizes = [192, 512],\n maskableSizes = [512],\n appleTouchIcon = 180,\n outDir = \"icons\",\n background = \"#ffffff\",\n maskablePadding = 0.1,\n appleSplash = false,\n splashBackground,\n splashIconScale = 0.3,\n } = options;\n\n const splashSpecs: AppleSplashSpec[] = appleSplash\n ? Array.isArray(appleSplash)\n ? appleSplash\n : DEFAULT_SPLASH\n : [];\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-icons\",\n apply: \"build\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n transformIndexHtml() {\n if (!splashSpecs.length) return;\n return splashSpecs.map((spec) => ({\n tag: \"link\",\n attrs: {\n rel: \"apple-touch-startup-image\",\n media: splashMedia(spec),\n href: `/${splashFileName(spec)}`,\n },\n injectTo: \"head\" as const,\n }));\n },\n async generateBundle() {\n let sharp: SharpFactory;\n try {\n // Non-literal specifier so TS doesn't require `sharp`'s types\n // (it is an optional, lazily-loaded dependency).\n const specifier = \"sharp\";\n const mod = (await import(specifier)) as { default?: SharpFactory } & SharpFactory;\n sharp = (mod.default ?? mod) as SharpFactory;\n } catch {\n this.warn(\n \"tempestPwaIcons: `sharp` is not installed — skipping icon generation. \" +\n \"Run `npm i -D sharp` to enable it.\",\n );\n return;\n }\n\n const input = await readFile(resolve(root, source));\n const bg = hexToRgb(background);\n const emit = (fileName: string, data: Buffer): void => {\n this.emitFile({ type: \"asset\", fileName, source: data });\n };\n\n // Regular \"any\" icons — transparent background, full bleed.\n for (const size of sizes) {\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(size, size, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n emit(`${outDir}/icon-${size}.png`, png);\n }\n\n // Maskable icons — content shrunk into the safe zone over a solid bg.\n for (const size of maskableSizes) {\n const content = Math.round(size * (1 - maskablePadding * 2));\n const png = await sharp(input, { density: Math.max(size, 512) })\n .resize(content, content, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .extend({\n top: Math.round((size - content) / 2),\n bottom: Math.round((size - content) / 2),\n left: Math.round((size - content) / 2),\n right: Math.round((size - content) / 2),\n background: { ...bg, alpha: 1 },\n })\n .resize(size, size)\n .png()\n .toBuffer();\n emit(`${outDir}/maskable-${size}.png`, png);\n }\n\n // Apple touch icon — opaque, no alpha.\n if (appleTouchIcon) {\n const png = await sharp(input, { density: Math.max(appleTouchIcon, 512) })\n .resize(appleTouchIcon, appleTouchIcon, {\n fit: \"contain\",\n background: { ...bg, alpha: 1 },\n })\n .flatten({ background: bg })\n .png()\n .toBuffer();\n emit(\"apple-touch-icon.png\", png);\n }\n\n // Apple splash screens — icon centered on a solid background.\n if (splashSpecs.length) {\n const splashBg = hexToRgb(splashBackground ?? background);\n for (const spec of splashSpecs) {\n const w = spec.width * spec.ratio;\n const h = spec.height * spec.ratio;\n const iconPx = Math.round(Math.min(w, h) * splashIconScale);\n const icon = await sharp(input, { density: Math.max(iconPx, 512) })\n .resize(iconPx, iconPx, {\n fit: \"contain\",\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toBuffer();\n const png = await sharp({\n create: {\n width: w,\n height: h,\n channels: 4,\n background: { ...splashBg, alpha: 1 },\n },\n })\n .composite([{ input: icon, gravity: \"center\" }])\n .png()\n .toBuffer();\n emit(splashFileName(spec), png);\n }\n }\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n\n/** Options for the sharp `create` (blank canvas) form. */\ninterface SharpCreate {\n create: {\n width: number;\n height: number;\n channels: number;\n background: { r: number; g: number; b: number; alpha: number };\n };\n}\n\n/** The sharp factory function (minimal typing — sharp is an optional dep). */\ntype SharpFactory = (input: Buffer | SharpCreate, opts?: { density?: number }) => SharpInstance;\n\n/** Minimal subset of the sharp chainable API this plugin uses. */\ninterface SharpInstance {\n resize(\n width: number,\n height: number,\n opts?: { fit?: string; background?: { r: number; g: number; b: number; alpha: number } },\n ): SharpInstance;\n extend(opts: {\n top: number;\n bottom: number;\n left: number;\n right: number;\n background: { r: number; g: number; b: number; alpha: number };\n }): SharpInstance;\n flatten(opts: { background: Rgb }): SharpInstance;\n composite(items: { input: Buffer; gravity?: string }[]): SharpInstance;\n png(): SharpInstance;\n toBuffer(): Promise<Buffer>;\n}\n","import { resolve } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** Options for {@link tempestPwaDevSw}. */\nexport interface TempestPwaDevSwOptions {\n /** Service-worker entry, relative to the project root. Default `src/sw.ts`. */\n swSrc?: string;\n /** URL the worker is served at (must match `registerServiceWorker`). Default `/sw.js`. */\n swUrl?: string;\n /** Dev URL of the precache manifest. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Serve the worker in dev. Default `true`; set `false` to opt out. */\n enabled?: boolean;\n}\n\n/**\n * Dev-server plugin that makes the service worker available under `npm run dev`.\n *\n * The production worker is bundled at build time (`vite.sw.config.ts`), so in\n * dev there is no `/sw.js` to register. This plugin compiles `swSrc` on the fly\n * with esbuild and serves it as a classic worker, plus an empty\n * `precache-manifest.json` (there are no hashed build assets to precache in\n * dev — push and runtime caching still work). It closes the \"SW in dev\" gap\n * that otherwise only `vite-plugin-pwa`'s `devOptions` covered.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestPwaDevSw } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestPwaDevSw()] });\n */\nexport function tempestPwaDevSw(options: TempestPwaDevSwOptions = {}): TempestVitePlugin {\n const {\n swSrc = \"src/sw.ts\",\n swUrl = \"/sw.js\",\n manifestUrl = \"/precache-manifest.json\",\n enabled = true,\n } = options;\n\n let root = process.cwd();\n\n const plugin: Plugin = {\n name: \"tempest-pwa-dev-sw\",\n apply: \"serve\",\n configResolved(config) {\n root = config.root ?? process.cwd();\n },\n configureServer(server) {\n if (!enabled) return;\n server.middlewares.use(async (req, res, next) => {\n const url = (req.url ?? \"\").split(\"?\")[0];\n\n if (url === swUrl) {\n try {\n const esbuild = await import(\"esbuild\");\n const result = await esbuild.build({\n entryPoints: [resolve(root, swSrc)],\n bundle: true,\n format: \"iife\",\n platform: \"browser\",\n target: \"es2020\",\n write: false,\n absWorkingDir: root,\n logLevel: \"silent\",\n });\n res.setHeader(\"Content-Type\", \"application/javascript\");\n res.setHeader(\"Service-Worker-Allowed\", \"/\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(result.outputFiles[0].text);\n } catch (error) {\n res.statusCode = 500;\n res.end(`// SW dev build failed:\\n// ${String(error)}`);\n }\n return;\n }\n\n if (url === manifestUrl) {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(JSON.stringify({ version: \"dev\", urls: [] }));\n return;\n }\n\n next();\n });\n },\n };\n\n return plugin as TempestVitePlugin;\n}\n"],"names":["normalizeProxy","proxy","out","path","value","createViteConfig","options","srcDir","port","host","open","alias","plugins","overrides","overridesConfig","base","react","resolve","merged","defineConfig","joinBase","file","hash","input","h","i","tempestPwaManifest","fileName","additionalUrls","exclude","includeHtml","appShell","config","_outputOptions","bundle","urls","list","version","DEFAULT_SPLASH","splashFileName","spec","splashMedia","hexToRgb","hex","full","c","tempestPwaIcons","source","sizes","maskableSizes","appleTouchIcon","outDir","background","maskablePadding","appleSplash","splashBackground","splashIconScale","splashSpecs","root","sharp","mod","readFile","bg","emit","data","size","png","content","splashBg","w","iconPx","icon","tempestPwaDevSw","swSrc","swUrl","manifestUrl","enabled","server","req","res","next","url","result","error"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tempest-react-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "SDK público da Tempest com componentes, hooks e integrações para projetos React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,11 +25,13 @@
25
25
  "dist",
26
26
  "bin",
27
27
  "template",
28
+ "template-pwa",
28
29
  "README.md",
29
30
  "LICENSE"
30
31
  ],
31
32
  "bin": {
32
- "create-tempest-app": "./bin/create-tempest-app.mjs"
33
+ "create-tempest-app": "./bin/create-tempest-app.mjs",
34
+ "tempest": "./bin/tempest.mjs"
33
35
  },
34
36
  "main": "./dist/tempest-react-sdk.cjs",
35
37
  "module": "./dist/tempest-react-sdk.js",
@@ -53,6 +55,11 @@
53
55
  "import": "./dist/vite.js",
54
56
  "require": "./dist/vite.cjs"
55
57
  },
58
+ "./sw": {
59
+ "types": "./dist/sw.d.ts",
60
+ "import": "./dist/sw.js",
61
+ "require": "./dist/sw.cjs"
62
+ },
56
63
  "./styles.css": "./dist/styles.css",
57
64
  "./package.json": "./package.json"
58
65
  },
@@ -68,6 +75,7 @@
68
75
  "test:coverage": "vitest run --coverage",
69
76
  "clean": "rm -rf dist coverage",
70
77
  "size": "size-limit",
78
+ "docs:llms": "node scripts/gen-llms.mjs",
71
79
  "prepare": "husky",
72
80
  "prepublishOnly": "npm run typecheck && npm run lint && npm run test:run && npm run build"
73
81
  },
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "tabWidth": 2,
5
+ "printWidth": 100,
6
+ "trailingComma": "all",
7
+ "arrowParens": "always",
8
+ "endOfLine": "lf"
9
+ }
@@ -2,6 +2,8 @@ import js from "@eslint/js";
2
2
  import globals from "globals";
3
3
  import reactHooks from "eslint-plugin-react-hooks";
4
4
  import reactRefresh from "eslint-plugin-react-refresh";
5
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
6
+ import unusedImports from "eslint-plugin-unused-imports";
5
7
  import tseslint from "typescript-eslint";
6
8
 
7
9
  export default tseslint.config(
@@ -16,12 +18,27 @@ export default tseslint.config(
16
18
  plugins: {
17
19
  "react-hooks": reactHooks,
18
20
  "react-refresh": reactRefresh,
21
+ "simple-import-sort": simpleImportSort,
22
+ "unused-imports": unusedImports,
19
23
  },
20
24
  rules: {
21
25
  ...reactHooks.configs.recommended.rules,
22
26
  "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
23
27
  "@typescript-eslint/consistent-type-imports": "error",
24
- "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
28
+ // Organize imports/exports (tempest fix).
29
+ "simple-import-sort/imports": "error",
30
+ "simple-import-sort/exports": "error",
31
+ // Remove dead imports + flag unused vars (tempest fix).
32
+ "@typescript-eslint/no-unused-vars": "off",
33
+ "unused-imports/no-unused-imports": "error",
34
+ "unused-imports/no-unused-vars": [
35
+ "warn",
36
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
37
+ ],
38
+ // Tidy whitespace (tempest fix).
39
+ "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0, maxBOF: 0 }],
40
+ "no-trailing-spaces": "error",
41
+ "eol-last": ["error", "always"],
25
42
  },
26
43
  },
27
44
  );
@@ -9,7 +9,10 @@
9
9
  "preview": "vite preview",
10
10
  "typecheck": "tsc --noEmit",
11
11
  "lint": "eslint .",
12
- "lint:fix": "eslint . --fix"
12
+ "lint:fix": "eslint . --fix",
13
+ "format": "prettier --write .",
14
+ "fix": "tempest fix",
15
+ "doctor": "tempest doctor"
13
16
  },
14
17
  "dependencies": {
15
18
  "react": "^19.0.0",
@@ -24,7 +27,10 @@
24
27
  "eslint": "^9.36.0",
25
28
  "eslint-plugin-react-hooks": "^5.2.0",
26
29
  "eslint-plugin-react-refresh": "^0.4.22",
30
+ "eslint-plugin-simple-import-sort": "^12.1.0",
31
+ "eslint-plugin-unused-imports": "^4.1.4",
27
32
  "globals": "^16.0.0",
33
+ "prettier": "^3.8.0",
28
34
  "typescript": "~5.9.0",
29
35
  "typescript-eslint": "^8.45.0",
30
36
  "vite": "^7.0.0"
@@ -0,0 +1,64 @@
1
+ # Tempest App (PWA)
2
+
3
+ Scaffolded with [`create-tempest-app --pwa`](https://www.npmjs.com/package/tempest-react-sdk) and powered by [`tempest-react-sdk`](https://www.npmjs.com/package/tempest-react-sdk).
4
+
5
+ ## Stack
6
+
7
+ - **Vite** with the `@` → `src` alias (`tempest-react-sdk/vite` → `createViteConfig`)
8
+ - **React Router v7** declarative routing (`defineRoutes` + `<AppRouter>`)
9
+ - **Zustand** state (`createAuthStore` + `createSelectors`)
10
+ - **TanStack Query** cache (mounted by `<AppProviders>`)
11
+ - **PWA**: installable manifest + a service worker built from `tempest-react-sdk/sw`, with web-push wiring
12
+
13
+ ## Getting started
14
+
15
+ ```bash
16
+ npm install
17
+ cp .env.example .env # adjust VITE_API_URL + VITE_VAPID_PUBLIC_KEY
18
+ npm run dev # http://127.0.0.1:5173
19
+ ```
20
+
21
+ ## PWA layout
22
+
23
+ ```text
24
+ public/manifest.webmanifest # install metadata (name, icons, theme color)
25
+ public/icon.svg # app icon (replace with your brand; PNG 192/512 recommended)
26
+ src/sw.ts # service worker — push + notificationclick + skip-waiting
27
+ vite.sw.config.ts # bundles src/sw.ts → dist/sw.js (classic worker)
28
+ index.html # manifest link + theme-color + apple meta tags
29
+ src/main.tsx # registers /sw.js in production, cleans up SW in dev
30
+ src/pages/Dashboard.tsx # Install button + push notifications toggle
31
+ ```
32
+
33
+ ## How it works
34
+
35
+ - **Install**: `index.html` links the manifest; `useBeforeInstallPrompt` (in
36
+ `Dashboard.tsx`) surfaces a custom Install button when the browser offers one.
37
+ - **Service worker**: `src/sw.ts` imports `installPushHandler`,
38
+ `installNotificationClickHandler` and `installSkipWaitingListener` from
39
+ `tempest-react-sdk/sw`. `npm run build` bundles it to `dist/sw.js` via
40
+ `vite.sw.config.ts`, and `src/main.tsx` registers it in production.
41
+ - **Web push**: `usePushSubscription` reads `VITE_VAPID_PUBLIC_KEY`, subscribes
42
+ through the active service worker, and hands the subscription to your
43
+ `onSubscribe` callback to POST to your backend.
44
+
45
+ > ⚠️ The service worker is bundled at **build time**, so push and offline behave
46
+ > only in a production build. Test them with:
47
+ >
48
+ > ```bash
49
+ > npm run build && npm run preview
50
+ > ```
51
+ >
52
+ > In `npm run dev` the worker is intentionally unregistered to avoid stale caches.
53
+
54
+ ## Replace the icons
55
+
56
+ `public/icon.svg` is a placeholder. For guaranteed installability across all
57
+ browsers, add PNG icons (192×192 and 512×512) to `public/` and point the
58
+ `manifest.webmanifest` `icons` entries at them.
59
+
60
+ ## Next steps
61
+
62
+ - Add a route: drop a page in `src/pages/` and an entry in `src/routes.tsx`.
63
+ - Fetch data: `useQuery({ queryKey: queryKeys.me(), queryFn: () => api.get("/me") })`.
64
+ - Generate VAPID keys on your backend and set `VITE_VAPID_PUBLIC_KEY` in `.env`.
@@ -0,0 +1,7 @@
1
+ # Base URL of your backend API (consumed by src/lib/api.ts).
2
+ VITE_API_URL=http://127.0.0.1:8000
3
+
4
+ # VAPID public key for web push (consumed by usePushSubscription in
5
+ # src/pages/Dashboard.tsx). Generate a key pair on your backend; the private
6
+ # half stays on the server, this public half ships to the browser.
7
+ VITE_VAPID_PUBLIC_KEY=
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
6
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
8
+ <meta name="theme-color" content="#0b1a36" />
9
+ <meta name="description" content="App React fiado com tempest-react-sdk." />
10
+ <meta name="application-name" content="Tempest App" />
11
+ <meta name="apple-mobile-web-app-capable" content="yes" />
12
+ <meta name="apple-mobile-web-app-title" content="Tempest App" />
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14
+ <meta name="mobile-web-app-capable" content="yes" />
15
+ <link rel="manifest" href="/manifest.webmanifest" />
16
+ <title>Tempest App</title>
17
+ </head>
18
+ <body>
19
+ <div id="root"></div>
20
+ <script type="module" src="/src/main.tsx"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,9 @@
1
+ {
2
+ "scripts": {
3
+ "build": "tsc --noEmit && vite build && npm run build:sw",
4
+ "build:sw": "vite build --config vite.sw.config.ts"
5
+ },
6
+ "devDependencies": {
7
+ "sharp": "^0.34.0"
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Tempest">
2
+ <rect width="512" height="512" rx="96" fill="#0b1a36" />
3
+ <path d="M152 168h208l-40 64h-72v176l-64 32V232h-72z" fill="#ffffff" />
4
+ </svg>
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "Tempest App",
3
+ "short_name": "Tempest",
4
+ "description": "App React fiado com tempest-react-sdk.",
5
+ "id": "/",
6
+ "start_url": "/?source=pwa",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "orientation": "portrait",
10
+ "lang": "pt-BR",
11
+ "dir": "ltr",
12
+ "background_color": "#ffffff",
13
+ "theme_color": "#0b1a36",
14
+ "categories": ["productivity", "utilities"],
15
+ "icons": [
16
+ {
17
+ "src": "/icons/icon-192.png",
18
+ "sizes": "192x192",
19
+ "type": "image/png",
20
+ "purpose": "any"
21
+ },
22
+ {
23
+ "src": "/icons/icon-512.png",
24
+ "sizes": "512x512",
25
+ "type": "image/png",
26
+ "purpose": "any"
27
+ },
28
+ {
29
+ "src": "/icons/maskable-512.png",
30
+ "sizes": "512x512",
31
+ "type": "image/png",
32
+ "purpose": "maskable"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,29 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { registerServiceWorker, skipWaiting } from "tempest-react-sdk";
4
+ import "tempest-react-sdk/styles.css";
5
+ import { App } from "@/App";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ );
12
+
13
+ // Register the service worker in both dev and prod. In production it's the
14
+ // bundled `/sw.js` (built by `vite.sw.config.ts`); in dev the `tempestPwaDevSw`
15
+ // plugin (in `vite.config.ts`) compiles and serves it on the fly, so push and
16
+ // runtime caching work under `npm run dev` too. Drop the `tempestPwaDevSw`
17
+ // plugin if you'd rather keep dev SW-free.
18
+ void registerServiceWorker({
19
+ url: "/sw.js",
20
+ onUpdate: (waiting) => {
21
+ // A new worker is ready. Prompt your users however you like; here we
22
+ // just activate it and reload so the next visit is up to date.
23
+ if (confirm("Nova versão disponível. Atualizar agora?")) {
24
+ skipWaiting(waiting);
25
+ window.location.reload();
26
+ }
27
+ },
28
+ onError: (err) => console.warn("[sw] registration failed", err),
29
+ });
@@ -0,0 +1,73 @@
1
+ import { Button, useBeforeInstallPrompt, usePushSubscription } from "tempest-react-sdk";
2
+ import { useAuth } from "@/stores/auth";
3
+
4
+ /**
5
+ * Protected page (see the `guard` in src/routes.tsx). Lazy-loaded, so it is
6
+ * code-split into its own chunk. Default export because `lazy` expects one.
7
+ *
8
+ * Adds two PWA controls:
9
+ * - Install button (`useBeforeInstallPrompt`) — appears only when the browser
10
+ * offers an install prompt and the app is not yet installed.
11
+ * - Push toggle (`usePushSubscription`) — subscribes/unsubscribes to web push.
12
+ * The service worker must be active, so this works in a production build
13
+ * (`npm run build && npm run preview`), not in `npm run dev`.
14
+ */
15
+ export default function Dashboard() {
16
+ const user = useAuth.use.user();
17
+ const install = useBeforeInstallPrompt();
18
+
19
+ const push = usePushSubscription({
20
+ vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY ?? "",
21
+ onSubscribe: async (subscription) => {
22
+ // Send the subscription to your backend so it can deliver pushes.
23
+ // await api.post("/webpush/subscribe", { body: subscription });
24
+ console.log("push subscription", subscription);
25
+ },
26
+ onUnsubscribe: async () => {
27
+ // await api.delete("/webpush/my");
28
+ console.log("push unsubscribed");
29
+ },
30
+ });
31
+
32
+ return (
33
+ <section>
34
+ <h1>Dashboard</h1>
35
+ <p>
36
+ Signed in as <strong>{user?.name}</strong> ({user?.email}).
37
+ </p>
38
+ <p>This route is only reachable while authenticated.</p>
39
+
40
+ <h2>PWA</h2>
41
+
42
+ {install.installed ? (
43
+ <p>✅ App installed.</p>
44
+ ) : install.installable ? (
45
+ <Button onClick={() => void install.prompt()}>Install app</Button>
46
+ ) : (
47
+ <p>Install prompt not available in this browser/context.</p>
48
+ )}
49
+
50
+ <h2>Notifications</h2>
51
+
52
+ {!push.supported ? (
53
+ <p>Web push is not supported here (needs HTTPS + an active service worker).</p>
54
+ ) : (
55
+ <>
56
+ <Button
57
+ onClick={() =>
58
+ void (push.subscribed ? push.unsubscribe() : push.subscribe())
59
+ }
60
+ disabled={push.loading}
61
+ >
62
+ {push.subscribed ? "Disable notifications" : "Enable notifications"}
63
+ </Button>
64
+ <p>
65
+ Permission: <strong>{push.permission}</strong> · Subscribed:{" "}
66
+ <strong>{String(push.subscribed)}</strong>
67
+ </p>
68
+ {push.error && <p style={{ color: "crimson" }}>{push.error.message}</p>}
69
+ </>
70
+ )}
71
+ </section>
72
+ );
73
+ }
@@ -0,0 +1,70 @@
1
+ /// <reference lib="webworker" />
2
+ import {
3
+ installBackgroundSync,
4
+ installNotificationClickHandler,
5
+ installPrecache,
6
+ installPushHandler,
7
+ installRuntimeCache,
8
+ installSkipWaitingListener,
9
+ } from "tempest-react-sdk/sw";
10
+
11
+ /**
12
+ * Service worker. Bundled to `dist/sw.js` by `vite.sw.config.ts` (see the
13
+ * `build:sw` script) and registered from `src/main.tsx`.
14
+ *
15
+ * Layers, in order:
16
+ * 1. Push + notifications + skip-waiting (the SDK push helpers).
17
+ * 2. Background sync — failed API mutations are queued and replayed online.
18
+ * 3. Runtime caching: API GETs (network-first) + media (cache-first with
19
+ * Range support, so audio/video seeking works offline).
20
+ * 4. Precache of the app shell so the app launches offline. Reads the
21
+ * `precache-manifest.json` emitted by `tempestPwaManifest()` in
22
+ * `vite.config.ts`.
23
+ *
24
+ * `installRuntimeCache` is registered BEFORE `installPrecache` so its specific
25
+ * routes win over the precache catch-all.
26
+ */
27
+ declare const self: ServiceWorkerGlobalScope;
28
+
29
+ installPushHandler({
30
+ defaultTitle: "Notificação",
31
+ defaultIcon: "/icon.svg",
32
+ defaultBadge: "/icon.svg",
33
+ });
34
+
35
+ installNotificationClickHandler();
36
+ installSkipWaitingListener();
37
+
38
+ // Queue failed POST/PUT/PATCH/DELETE to /api and replay them when back online.
39
+ installBackgroundSync({ match: (url) => url.pathname.startsWith("/api/") });
40
+
41
+ installRuntimeCache([
42
+ {
43
+ match: (url) => url.pathname.startsWith("/api/"),
44
+ strategy: "network-first",
45
+ cacheName: "api",
46
+ networkTimeoutSeconds: 5,
47
+ maxEntries: 50,
48
+ maxAgeSeconds: 60 * 5,
49
+ },
50
+ {
51
+ // Audio/video: cache-first + Range support (seek/scrub works offline).
52
+ match: (url) => /\.(mp3|mp4|webm|ogg|wav)$/.test(url.pathname),
53
+ strategy: "cache-first",
54
+ cacheName: "media",
55
+ maxEntries: 20,
56
+ rangeRequests: true,
57
+ },
58
+ ]);
59
+
60
+ installPrecache({
61
+ navigateFallback: "/index.html",
62
+ // Don't serve the app shell for API navigations.
63
+ navigateFallbackDenylist: [/^\/api\//],
64
+ });
65
+
66
+ // Take control of open pages as soon as this worker activates. `installPrecache`
67
+ // also calls `clients.claim()`; this is harmless if you drop precaching.
68
+ self.addEventListener("activate", (event) => {
69
+ event.waitUntil(self.clients.claim());
70
+ });
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ /** Base URL of the backend API (src/lib/api.ts). */
5
+ readonly VITE_API_URL?: string;
6
+ /** VAPID public key for web push (src/pages/Dashboard.tsx). */
7
+ readonly VITE_VAPID_PUBLIC_KEY?: string;
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ createViteConfig,
3
+ tempestPwaDevSw,
4
+ tempestPwaIcons,
5
+ tempestPwaManifest,
6
+ } from "tempest-react-sdk/vite";
7
+
8
+ // `createViteConfig` wires `@vitejs/plugin-react`, the `@` → `src` alias and
9
+ // dev-server defaults. The three PWA plugins (order matters):
10
+ // - tempestPwaIcons — rasterizes public/icon.svg into the PNG icon set (sharp).
11
+ // - tempestPwaManifest — emits dist/precache-manifest.json (it sees the icons
12
+ // above because they're emitted first), for offline app-shell precaching.
13
+ // - tempestPwaDevSw — serves /sw.js (and an empty manifest) under `npm run dev`.
14
+ export default createViteConfig({
15
+ // proxy: { "/api": "http://127.0.0.1:8000" },
16
+ plugins: [
17
+ tempestPwaIcons({ source: "public/icon.svg", appleSplash: true }),
18
+ tempestPwaManifest({ additionalUrls: ["/manifest.webmanifest", "/icon.svg"] }),
19
+ tempestPwaDevSw(),
20
+ ],
21
+ });
@@ -0,0 +1,27 @@
1
+ import { resolve } from "node:path";
2
+ import { defineConfig } from "vite";
3
+
4
+ /**
5
+ * Standalone build that bundles `src/sw.ts` (and the `tempest-react-sdk/sw`
6
+ * helpers it imports) into a single classic service worker at `dist/sw.js`.
7
+ *
8
+ * Run after the app build via the `build:sw` script — `emptyOutDir: false`
9
+ * keeps the app's `dist/` intact. The IIFE format produces a worker with no
10
+ * `import`/`export` tokens, so it registers as a classic worker (no
11
+ * `type: "module"` needed).
12
+ */
13
+ export default defineConfig({
14
+ build: {
15
+ emptyOutDir: false,
16
+ sourcemap: true,
17
+ lib: {
18
+ entry: resolve(__dirname, "src/sw.ts"),
19
+ formats: ["iife"],
20
+ name: "sw",
21
+ fileName: () => "sw.js",
22
+ },
23
+ rollupOptions: {
24
+ output: { entryFileNames: "sw.js", inlineDynamicImports: true },
25
+ },
26
+ },
27
+ });