# @uschuster/webapp-scaffold Shared frontend build glue for webapp-template-derived projects. ## What's in v0.1 | Path | Purpose | |------|---------| | `bin/fetch-openapi.sh` | Pull the oatpp Swagger spec from a running backend. Configurable via `OPENAPI_URL` / `APP_URL` / `APP_TEST_PORT`, optional Bearer via `APP_API_KEY`. JSON-validated. | | `bin/postprocess-openapi.py` | Clean up oatpp 1.3's rough edges (missing `operationId`s, missing tags) before handing the spec to Orval. | | `bin/inject-hashed-filenames.py` | Rewrite HTML script tags to point at Vite's manifest-declared hashed bundle. Config-driven so projects with multiple entry points (admin + guest) use a single invocation. | | `src/vite-config.ts` | `defineAdminConfig({root, vendorChunks?, outDir?})` and `defineGuestConfig({root, entry?, vendorChunks?, outDir?})` helpers that bake in the `VITE_BASE`-driven prefix convention, manifest output, and the `static/{dist,guest/dist}` output layout the `StaticController` expects. | | `templates/orval.config.template.ts` | Starting `orval.config.ts` for derived projects to copy-and-tweak. | ## Install ```json { "devDependencies": { "@uschuster/webapp-scaffold": "^0.1.0" } } ``` Register the internal Forgejo npm registry (see your `~/.npmrc`): ``` @uschuster:registry=http://127.0.0.1:3000/api/packages/uwe.admin/npm/ ``` ## Consumer wiring ```ts // frontend/vite.config.ts import { defineAdminConfig } from '@uschuster/webapp-scaffold'; export default defineAdminConfig({ root: __dirname }); ``` ```sh # frontend/package.json > scripts > codegen webapp-scaffold-fetch-openapi && \ webapp-scaffold-postprocess-openapi && \ orval ``` ## `createCoreFetch` (v0.4 — default flipped to `'body'`) Orval's `client: 'fetch'` calls a mutator `coreFetch(url, init)`. The return shape is configurable; **the default is `'body'`** (returns the parsed JSON body directly, matching Orval's `includeHttpResponseReturnType: false`). Pass `responseShape: 'wrapped'` to opt back into `{data, status, headers}` when callers need the Response metadata. `createCoreFetch(opts)` returns such a function, with credentials/CSRF headers / 204 / content-type handling already wired, and hooks for project-specific concerns: ```ts import { createCoreFetch } from '@uschuster/webapp-scaffold/core-fetch'; export const coreFetch = createCoreFetch({ baseUrl: import.meta.env.BASE_URL, syncTables: new Set(['bookings', 'persons', 'contacts']), onEnqueue: (req) => syncQueue.push(req), // returns true ⇒ skip network on401: () => window.location.assign('/admin'), on409: (_req, body) => ({ conflict: JSON.parse(body) }), }); ``` ## i18n (v0.3) Two axes: **locale** (`de`, `en`, …) × **tone** (`formal`, `informal`). Bundles are keyed by `` (fallback) and `-` (primary); the resolver walks `-` → `` → `-` → `` → the raw key (missing keys log via an `onMiss` hook instead of breaking render). ```ts // Define bundles — only strings that differ between tones need a // tone-specific entry. Everything else lives in the locale bundle. import { createI18n } from '@uschuster/webapp-scaffold/i18n'; import { useI18nStore } from '@uschuster/webapp-scaffold/i18n-react'; // React-only const bundles = { 'de': { greeting: 'Willkommen', see_more: 'Mehr anzeigen' }, 'de-informal': { greeting: 'Hi!' }, 'en': { greeting: 'Welcome', see_more: 'See more' }, } as const; export const i18n = createI18n({ bundles, defaultLocale: 'de', defaultTone: 'formal', onMiss: (key, loc, tone) => console.warn(`[i18n] missing ${String(key)} @ ${loc}-${tone}`), }); // React — components re-render on setLocale / setTone: export const useI18n = () => useI18nStore(i18n); // In a component: const { t, setTone } = useI18n(); return ; ``` SSR-safe: pass `initialLocale` / `initialTone` from the server render. Tree-shakeable: only the bundles you import ship in the bundle.