No description
Find a file
Uwe Schuster 4c4d52e3de postprocess-openapi: strip empty required: [] arrays
oatpp 1.3 emits `"required": []` on schemas with no required fields.
OpenAPI 3.0.x rejects this with "must NOT have fewer than 1 items",
which silently breaks strict consumers. Orval's typed fetch client
swallowed it (the violation surfaces only as a non-fatal warning), but
Orval's Zod generator (added downstream in fewo-webapp's #469 increment 2)
fails hard:

  must NOT have fewer than 1 items at /components/schemas/<DTO>/required
  must have required property '$ref'                  at /components/schemas/<DTO>
  must match exactly one schema in oneOf              at /components/schemas/<DTO>

83 of fewo-webapp's 198 DTOs trip this. Stripping the empty array (absence
of the keyword has the same semantics) is the spec-compliant fix.

Bumps to v0.4.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:22:30 +02:00
bin postprocess-openapi: strip empty required: [] arrays 2026-05-05 21:22:30 +02:00
src #5: createCoreFetch default responseShape flipped to 'body' 2026-04-25 21:47:40 +02:00
templates v0.1.0: fetch/postprocess/inject scripts + vite config factories 2026-04-21 22:06:58 +02:00
tests #7: Add tests/e2e/ for the initial password-setup flow (Option A1) 2026-04-25 22:23:30 +02:00
.gitignore gitignore: drop accidentally-committed __pycache__/.pytest_cache 2026-04-25 21:38:32 +02:00
package-lock.json #4: vitest harness + suites for createCoreFetch / createI18n / useI18nStore 2026-04-25 21:45:31 +02:00
package.json postprocess-openapi: strip empty required: [] arrays 2026-05-05 21:22:30 +02:00
README.md #5: createCoreFetch default responseShape flipped to 'body' 2026-04-25 21:47:40 +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
vitest.config.ts #4: vitest harness + suites for createCoreFetch / createI18n / useI18nStore 2026-04-25 21:45:31 +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.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:

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.