Lets consumers pass `() => import.meta.env.BASE_URL` without orval's CJS bundle path blowing up on the top-level `import.meta` reference. The getter is invoked at request time rather than factory time, so the mutator file can be loaded by orval (which bundles to CJS) without evaluating `import.meta`. Closes uwe.admin/webapp-template#21 (scaffold side). Bump to v0.5.0 — additive change (string form still works) but new shape for `CoreFetchOptions.baseUrl` is a public API change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| bin | ||
| src | ||
| templates | ||
| tests | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@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.