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>
106 lines
4.1 KiB
Markdown
106 lines
4.1 KiB
Markdown
# @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.
|