New options to fit fewo's core-fetch contract:
responseShape: 'wrapped' | 'body' — 'body' matches orval's
includeHttpResponseReturnType:false
onNetworkFailure(req, error) — return a value to swallow fetch()
rejection; enables online-first +
enqueue-on-offline pattern (fewo)
formatError(req, resp, text, json) — override default error message
format (needed for localized 401/403
messages, conflict-body decoration)
409 errors throw an Error decorated with .status = 409 and .body = parsed
JSON so conflict resolvers at call sites can still inspect them.
Backwards-compatible: new options default to prior behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| bin | ||
| src | ||
| templates | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@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 operationIds, 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
{
"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
// frontend/vite.config.ts
import { defineAdminConfig } from '@uschuster/webapp-scaffold';
export default defineAdminConfig({ root: __dirname });
# frontend/package.json > scripts > codegen
webapp-scaffold-fetch-openapi && \
webapp-scaffold-postprocess-openapi && \
orval
createCoreFetch (v0.2)
Orval's client: 'fetch' calls a mutator coreFetch<T>(url, init) and
expects {data, status, headers} back. createCoreFetch(opts) returns
such a function, with credentials/CSRF headers / 204 / content-type
handling already wired, and hooks for project-specific concerns:
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).
// 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.