@finesoft/front
v0.1.59
Published
Full-stack framework: router, DI, actions, SSR, and server — all in one package
Readme
@finesoft/front
Full-stack TypeScript framework — router, DI, actions, SSR, and server — all in one package.
Works with Vue, React, and Svelte. Deploy to Node.js, Vercel, Cloudflare Workers, Netlify, or static hosting.
Install
npx create-finesoft-app my-appOr add to an existing project:
npm install @finesoft/frontPeer dependencies: hono >= 4.0.0. Optional: @hono/node-server, vite >= 5.0.0.
Setup
// vite.config.ts
import { finesoftFrontViteConfig } from "@finesoft/front";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
vue(),
finesoftFrontViteConfig({
ssr: { entry: "src/ssr.ts" },
i18n: { messagesDir: "src/locales" },
proxies: [{ prefix: "/api", target: "https://api.example.com" }],
adapter: "auto",
}),
],
});Routing
// src/bootstrap.ts
import { type Framework, defineRoutes } from "@finesoft/front";
import { HomeController } from "./lib/controllers/home";
import { authGuard } from "./lib/guards/auth";
export function bootstrap(framework: Framework): void {
defineRoutes(framework, [
{ path: "/", intentId: "home", controller: new HomeController() },
{
path: "/about",
intentId: "about",
controller: new AboutController(),
renderMode: "csr",
},
{ path: "/admin", intentId: "home", beforeLoad: [authGuard] },
]);
}Controllers
import { BaseController, type Container } from "@finesoft/front";
class HomeController extends BaseController<Record<string, string>, HomePage> {
readonly intentId = "home";
async execute(_params: Record<string, string>, container: Container) {
const http = container.resolve<HttpClient>("http");
return http.get("/api/home");
}
}Middleware
Two-phase guards shared between SSR and CSR:
import { next, redirect, type NavigationContext } from "@finesoft/front";
function authGuard(ctx: NavigationContext) {
return ctx.getCookie("token") ? next() : redirect("/login");
}Results: next(), redirect(url, status?), rewrite(url), deny(status?, message?).
Lifecycle Hooks
startBrowserApp provides hooks for initialization:
import { startBrowserApp } from "@finesoft/front/browser";
startBrowserApp({
bootstrap,
// Pass framework config so locale/reporting/etc. are wired into Framework
frameworkConfig: {
locale: "zh-Hans",
},
mount: (target, { framework }) => {
/* ... */
},
callbacks: { onNavigate, onExternalUrl },
onBeforeStart(framework) {
// Runs after Framework creation, before mount.
// Good for: error monitoring, analytics SDK, i18n init.
},
onAfterStart(framework) {
// Runs after initial page trigger.
// Good for: service worker registration, performance marks.
},
});Locale (i18n)
Pass locale in FrameworkConfig to enable automatic locale handling:
const framework = Framework.create({
locale: "zh-Hans", // or "en-US", "ar-SA", etc.
});
// SSR: automatically injects <html lang="zh-Hans" dir="ltr">
// Browser: automatically sets document.documentElement.lang/dir on startupFor SSR, locale is resolved in this order:
- Explicit
resolveLocalecallback (if provided) localefromFrameworkConfig(via DI container)
Access at runtime:
const locale = framework.getLocale();
// { lang: "zh-Hans", dir: "ltr" }Translator
For synchronous in-memory i18n translation, use SimpleTranslator:
import { SimpleTranslator } from "@finesoft/front";
const t = new SimpleTranslator({
locale: "zh-Hans",
messages: {
hello: "你好",
"items.one": "{count} 个项目",
"items.other": "{count} 个项目",
},
});
t.t("hello"); // "你好"
t.t("hello", { name: "World" }); // "你好"
t.plural("items", 5); // "5 个项目"For locale dictionaries, prefer JSON files plus finesoftFrontViteConfig(). The framework
will automatically load ${locale}.json before SSR render and before browser hydration,
without serializing the translations into HTML.
// vite.config.ts
import { finesoftFrontViteConfig } from "@finesoft/front";
import { defineConfig } from "vite-plus";
export default defineConfig({
plugins: [
finesoftFrontViteConfig({
ssr: { entry: "src/ssr.ts" },
i18n: {
messagesDir: "src/locales",
},
}),
],
});
// src/locales/zh-Hans.json
{
"hello": "你好"
}
// src/locales/en-US.json
{
"hello": "Hello"
}If you need a non-file source such as a CDN or API, loadMessages is still supported on
createSSRRender() / startBrowserApp() and overrides the Vite-generated loader.
RTL Support
import { isRtl, getTextDirection, getLocaleAttributes } from "@finesoft/front";
isRtl("ar"); // true
getTextDirection("he"); // "rtl"
getLocaleAttributes("ar-SA"); // { lang: "ar-SA", dir: "rtl" }Metrics & Event Recording
EventRecorder (recommended)
New pipeline for structured event recording. Default: ConsoleEventRecorder (logs to console).
import { Framework, type EventRecorder } from "@finesoft/front";
// Custom recorder
const framework = Framework.create({
eventRecorder: myAnalyticsRecorder,
});
// Framework automatically records PageView events via didEnterPage()Compose multiple recorders:
import { CompositeEventRecorder, ConsoleEventRecorder } from "@finesoft/front";
const recorder = new CompositeEventRecorder([new ConsoleEventRecorder(), myProductionRecorder]);Inject common fields into every event:
import { WithFieldsRecorder } from "@finesoft/front";
const recorder = new WithFieldsRecorder(baseRecorder, [
{ getFields: () => ({ app: "myApp", version: "1.0" }) },
]);Impression Tracking
Track element visibility using IntersectionObserver:
import { IntersectionImpressionObserver } from "@finesoft/front";
const observer = new IntersectionImpressionObserver((entries) => {
for (const entry of entries) {
analytics.track("impression", { id: entry.id, ...entry.metadata });
}
});
observer.observe(element, "product-card-123", { category: "featured" });
// Later:
observer.unobserve(element);
observer.destroy();Error Reporting
Send warn/error logs to an external monitoring service (Sentry, Datadog, etc.):
import { Framework, type ReportCallback } from "@finesoft/front";
const framework = Framework.create({
reportCallback(level, category, args) {
sentry.captureMessage(`[${category}] ${args.join(" ")}`, level);
},
});
// Automatically composes with ConsoleLogger — console output is preserved.
// All framework.getLogger().warn(...) and .error(...) calls are forwarded.HTTP Client
Subclass HttpClient to create typed API clients:
import { HttpClient } from "@finesoft/front";
class MyApi extends HttpClient {
async getUser(id: string) {
return this.get<User>(`/users/${id}`);
}
async createUser(data: NewUser) {
return this.post<User>("/users", data);
}
}Interceptors
Add request/response interceptors for auth, logging, retries, etc.:
const api = new MyApi({
baseUrl: "/api",
requestInterceptors: [
(url, init) => {
init.headers = {
...init.headers,
Authorization: `Bearer ${token}`,
};
return init;
},
],
responseInterceptors: [
(response, url) => {
if (response.status === 401) refreshToken();
return response;
},
],
});
// Or add dynamically:
api.useRequestInterceptor((url, init) => {
/* ... */ return init;
});Platform Detection
Automatically detected from User-Agent and available via DI:
const platform = framework.getPlatform();
// { os: "ios", browser: "safari", engine: "webkit", isMobile: true, isTouch: true }Standalone usage:
import { detectPlatform } from "@finesoft/front";
const info = detectPlatform(); // auto-reads navigator.userAgentPWA Detection
import { getPWADisplayMode } from "@finesoft/front";
const mode = getPWADisplayMode();
// "standalone" | "twa" | "browser"Feature Flags
const framework = Framework.create({
featureFlags: { darkMode: true, maxRetries: 3 },
});
// Add remote providers (last registered wins):
import { type FeatureFlagsProvider } from "@finesoft/front";
const framework = Framework.create({
featureFlags: { darkMode: false },
featureFlagsProviders: [remoteConfigProvider],
});DI Container
Scoped containers for request isolation (SSR):
const requestScope = framework.container.createScope();
requestScope.register("user", () => currentUser);
// Falls back to parent container if key not foundSSR
// src/ssr.ts
import { createSSRRender, serializeServerData } from "@finesoft/front";
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";
import App from "./App.vue";
import { bootstrap } from "./bootstrap";
export const render = createSSRRender({
bootstrap,
getErrorPage: () => ({ title: "Error", kind: "error" }),
async renderApp(page) {
const html = await renderToString(createSSRApp(App, { page }));
return { html, head: `<title>${page.title}</title>`, css: "" };
},
});
export { serializeServerData };Adapters
| Adapter | Target |
| -------------- | -------------------------- |
| "node" | Standalone Node.js server |
| "vercel" | Vercel Build Output API v3 |
| "cloudflare" | Cloudflare Workers |
| "netlify" | Netlify Functions v2 |
| "static" | Pre-rendered static files |
| "auto" | Auto-detect at build time |
Entry Points
| Import | Contents |
| ------------------------- | ------------------------------------------ |
| @finesoft/front | Everything (core + browser + SSR + server) |
| @finesoft/front/browser | Browser-only (no server code) |
License
MIT
