No description
Find a file
Uwe Schuster 0673c4c0d8 v0.3.4: error path prefers .json() with .text() fallback; always decorate 409
Matches the common test pattern of mocking .json() for error responses
(vitest helpers often do that by default and don't bother with .text()).
The error path now clones the response, tries .json() first, and falls
back to .text() on parse failure. Populates both text and json for
formatError callers.

409 errors are always decorated with .status=409 and .body=<json>, even
when the consumer doesn't provide on409 — conflict resolvers at call
sites can still reach the payload.

Default 409 message is the body's .message field if present; otherwise
falls through to the usual format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:31:56 +02:00
bin v0.1.0: fetch/postprocess/inject scripts + vite config factories 2026-04-21 22:06:58 +02:00
src v0.3.4: error path prefers .json() with .text() fallback; always decorate 409 2026-04-21 22:31:56 +02:00
templates v0.1.0: fetch/postprocess/inject scripts + vite config factories 2026-04-21 22:06:58 +02:00
.gitignore v0.3.1: ship compiled dist/ so npm git installs work without TS stripping 2026-04-21 22:23:36 +02:00
package-lock.json v0.3.1: ship compiled dist/ so npm git installs work without TS stripping 2026-04-21 22:23:36 +02:00
package.json v0.3.4: error path prefers .json() with .text() fallback; always decorate 409 2026-04-21 22:31:56 +02:00
README.md v0.3.0: i18n system (locale × tone) with fallback chain 2026-04-21 22:12:18 +02:00
tsconfig.json v0.3.1: ship compiled dist/ so npm git installs work without TS stripping 2026-04-21 22:23:36 +02:00

@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.