Consumers installing via 'git+http://.../webapp-scaffold.git#<tag>' hit Node's ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING because .ts files under node_modules can't be loaded by vite.config.ts loaders / node's default loader. Add tsconfig.json + 'prepare: tsc' (runs on git install). Emit .js + .d.ts into dist/. package.json exports point at dist/; .ts remains in src/ for direct-source consumers (e.g. monorepo setups). 'dist/' is gitignored — it's a build artifact, populated at install time. Version bump to 0.3.1 since this is a patch on the already-released 0.3.0 ABI (no API changes, just packaging). 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.