webapp-scaffold/README.md
Uwe Schuster fd451fd452 #5: createCoreFetch default responseShape flipped to 'body'
Orval's `client: 'fetch'` (with the no-`includeHttpResponseReturnType`
default) emits mutator signatures expecting the parsed body — so making
`'body'` the default means the no-arg `createCoreFetch()` form is the
right answer for the typical Orval-driven derived project. Callers that
need Response metadata opt in via `responseShape: 'wrapped'`.

Aligns README + docstring + the inline default. Tests updated to assert
default-`'body'` and explicit-`'wrapped'`.

Bump to 0.4.0 (BREAKING — default flipped). Downstream consumers on the
default need to either (a) add explicit `responseShape: 'wrapped'` to
preserve old behaviour, or (b) migrate callers to the body shape.
fewo-webapp's existing `responseShape: 'body'` override is now redundant
and will be dropped in a follow-up.

Closes #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:47:40 +02:00

106 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @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<T>(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 `<locale>` (fallback) and `<locale>-<tone>` (primary);
the resolver walks `<cur-locale>-<cur-tone>``<cur-locale>`
`<default-locale>-<default-tone>``<default-locale>` → 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<typeof bundles['de']>({
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 <button onClick={() => setTone('informal')}>{t('greeting')}</button>;
```
SSR-safe: pass `initialLocale` / `initialTone` from the server render.
Tree-shakeable: only the bundles you import ship in the bundle.