Skip to content

Commit 257eb7c

Browse files
authored
Merge pull request #16 from script-development/docs/vitepress-documentation-site
docs: add VitePress documentation site
2 parents f6f5c73 + 384a29b commit 257eb7c

18 files changed

Lines changed: 5077 additions & 595 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ reports/
55
.stryker-tmp/
66
.stryker-incremental.json
77
*.tsbuildinfo
8+
docs/.vitepress/cache/
89
.changeset/*.md
910
!.changeset/config.json

docs/.vitepress/config.mts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { defineConfig } from "vitepress";
2+
3+
export default defineConfig({
4+
title: "FS Packages",
5+
description: "Shared frontend service packages by Script Development",
6+
7+
themeConfig: {
8+
nav: [
9+
{ text: "Home", link: "/" },
10+
{ text: "Getting Started", link: "/getting-started" },
11+
{ text: "Architecture", link: "/architecture" },
12+
{ text: "Packages", link: "/packages/http" },
13+
{ text: "Contributing", link: "/contributing" },
14+
],
15+
16+
sidebar: {
17+
"/": [
18+
{
19+
text: "Guide",
20+
items: [
21+
{ text: "Getting Started", link: "/getting-started" },
22+
{ text: "Architecture", link: "/architecture" },
23+
{ text: "Contributing", link: "/contributing" },
24+
],
25+
},
26+
{
27+
text: "Foundation",
28+
collapsed: false,
29+
items: [
30+
{ text: "fs-http", link: "/packages/http" },
31+
{ text: "fs-storage", link: "/packages/storage" },
32+
{ text: "fs-helpers", link: "/packages/helpers" },
33+
],
34+
},
35+
{
36+
text: "Services",
37+
collapsed: false,
38+
items: [
39+
{ text: "fs-theme", link: "/packages/theme" },
40+
{ text: "fs-loading", link: "/packages/loading" },
41+
{ text: "fs-toast", link: "/packages/toast" },
42+
{ text: "fs-dialog", link: "/packages/dialog" },
43+
{ text: "fs-translation", link: "/packages/translation" },
44+
],
45+
},
46+
{
47+
text: "Domain",
48+
collapsed: false,
49+
items: [
50+
{ text: "fs-adapter-store", link: "/packages/adapter-store" },
51+
{ text: "fs-router", link: "/packages/router" },
52+
],
53+
},
54+
],
55+
},
56+
57+
socialLinks: [
58+
{ icon: "github", link: "https://github.com/script-development/fs-packages" },
59+
{ icon: "npm", link: "https://www.npmjs.com/org/script-development" },
60+
],
61+
62+
search: {
63+
provider: "local",
64+
},
65+
66+
outline: {
67+
level: [2, 3],
68+
},
69+
70+
footer: {
71+
message: "Built by Script Development & Back to Code",
72+
},
73+
},
74+
});

docs/architecture.md

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# Architecture
2+
3+
This page explains the design decisions behind the package collection. If you're looking for "how do I use this?" — start with [Getting Started](/getting-started). This page answers "why is it built this way?"
4+
5+
## The Factory Pattern
6+
7+
Every package exports a `createXxxService()` function that returns a plain object:
8+
9+
```typescript
10+
const http = createHttpService("https://api.example.com");
11+
const storage = createStorageService("myapp");
12+
const loading = createLoadingService();
13+
```
14+
15+
### Why not classes?
16+
17+
Classes create hidden coupling. When you `new` a class, you commit to an inheritance chain, a `this` binding, and often a global singleton pattern. Our factories return plain objects — there's no prototype chain, no `this` to lose, no base class to accidentally override.
18+
19+
```typescript
20+
// What you get back is just an object with methods
21+
const http = createHttpService("https://api.example.com");
22+
23+
// You can destructure it, pass individual methods around, or store it — no surprises
24+
const { getRequest, postRequest } = http;
25+
```
26+
27+
### Why not singletons?
28+
29+
Singletons are convenient until you need two instances. A factory lets you create as many instances as you need:
30+
31+
```typescript
32+
// Two HTTP services pointing at different APIs
33+
const apiHttp = createHttpService("https://api.example.com");
34+
const authHttp = createHttpService("https://auth.example.com");
35+
36+
// Two storage services with different prefixes
37+
const userStorage = createStorageService("user");
38+
const cacheStorage = createStorageService("cache");
39+
```
40+
41+
In testing, factories make setup trivial — create a fresh service per test, no global state to reset.
42+
43+
## Loose Coupling
44+
45+
Packages avoid importing each other directly. Instead, they define the **shape** of what they need and accept anything that matches.
46+
47+
### Structural Typing (Duck Types)
48+
49+
The clearest example is `fs-theme`. It needs something that can `get()` and `put()` values — but it doesn't care whether that's `fs-storage`, a wrapper around IndexedDB, or a mock:
50+
51+
```typescript
52+
// fs-theme defines what it needs
53+
interface ThemeStorageContract {
54+
get<T>(key: string): T | undefined;
55+
put(key: string, value: unknown): void;
56+
}
57+
58+
// fs-storage happens to match — but theme doesn't import it
59+
const storage = createStorageService("myapp");
60+
const theme = createThemeService(storage); // works because the shape matches
61+
```
62+
63+
::: tip What does this buy you?
64+
In tests, you can pass a plain object instead of a real storage service:
65+
66+
```typescript
67+
const fakeStorage = { get: () => undefined, put: () => {} };
68+
const theme = createThemeService(fakeStorage);
69+
```
70+
71+
No mocking library, no dependency injection framework. Just an object with the right shape.
72+
:::
73+
74+
### Why peer dependencies?
75+
76+
When packages do depend on each other (like `fs-loading` using `fs-http`'s middleware), the dependency is declared as a **peer dependency**. This means:
77+
78+
1. Your application installs a single copy of each package
79+
2. Packages share the same instance at runtime
80+
3. There are no version conflicts from nested `node_modules`
81+
82+
```json
83+
// fs-loading's package.json
84+
{
85+
"peerDependencies": {
86+
"@script-development/fs-http": "^1.0.0",
87+
"vue": "^3.5.0"
88+
}
89+
}
90+
```
91+
92+
## Middleware Architecture
93+
94+
Several packages support **middleware** — functions you register to intercept and react to events. Every middleware registration returns an **unregister function** for clean teardown.
95+
96+
### The HTTP Middleware Pipeline
97+
98+
`fs-http` provides three middleware hooks that form a request lifecycle:
99+
100+
```typescript
101+
const http = createHttpService("https://api.example.com");
102+
103+
// 1. Before the request goes out
104+
const unregReq = http.registerRequestMiddleware((config) => {
105+
config.headers.set("Authorization", `Bearer ${token}`);
106+
});
107+
108+
// 2. When a successful response comes back
109+
const unregRes = http.registerResponseMiddleware((response) => {
110+
trackAnalytics(response.config.url, response.status);
111+
});
112+
113+
// 3. When a request fails
114+
const unregErr = http.registerResponseErrorMiddleware((error) => {
115+
if (error.response?.status === 401) {
116+
redirectToLogin();
117+
}
118+
});
119+
120+
// Clean up when done
121+
unregReq();
122+
unregRes();
123+
unregErr();
124+
```
125+
126+
### Cross-Package Middleware Composition
127+
128+
The middleware system enables packages to compose without direct coupling. `fs-loading` doesn't modify `fs-http` — it hooks into it:
129+
130+
```typescript
131+
import { createHttpService } from "@script-development/fs-http";
132+
import { createLoadingService, registerLoadingMiddleware } from "@script-development/fs-loading";
133+
134+
const http = createHttpService("https://api.example.com");
135+
const loading = createLoadingService();
136+
137+
// This registers request + response + error middleware on the HTTP service
138+
const { unregister } = registerLoadingMiddleware(http, loading);
139+
140+
// Now loading.isLoading automatically reflects pending HTTP requests
141+
// When done, clean up all three middleware registrations at once:
142+
unregister();
143+
```
144+
145+
The same pattern appears in `fs-dialog` (error middleware) and `fs-router` (navigation middleware).
146+
147+
### Why the unregister pattern?
148+
149+
In single-page applications, services outlive individual components. If a component registers middleware and then unmounts, those handlers must be cleaned up to prevent memory leaks and stale behavior:
150+
151+
```typescript
152+
// In a Vue composable or component setup
153+
const unregister = http.registerResponseErrorMiddleware((error) => {
154+
showErrorNotification(error);
155+
});
156+
157+
// Clean up on unmount
158+
onUnmounted(() => {
159+
unregister();
160+
});
161+
```
162+
163+
## Component Agnosticism
164+
165+
`fs-toast` and `fs-dialog` manage **state and lifecycle** — the queue, the stack, the open/close logic — but they don't render anything. You provide the Vue component, they handle the plumbing:
166+
167+
```typescript
168+
import { createToastService } from "@script-development/fs-toast";
169+
import MyToast from "@/components/MyToast.vue"; // YOUR component
170+
171+
const toast = createToastService(MyToast);
172+
173+
// Show a toast — props are type-checked against your component
174+
toast.show({ message: "Saved", type: "success" });
175+
```
176+
177+
### Why not built-in components?
178+
179+
Built-in UI components force design decisions on you — colors, animations, positioning, accessibility patterns. Our projects have different design systems. By separating the service (queue management, stack management, lifecycle) from the presentation (your component), each project gets the behavior without the opinions.
180+
181+
The service provides a `ContainerComponent` that you mount once in your app root:
182+
183+
```vue
184+
<template>
185+
<div id="app">
186+
<router-view />
187+
<toast.ToastContainerComponent />
188+
<dialog.DialogContainerComponent />
189+
</div>
190+
</template>
191+
```
192+
193+
The container handles rendering, ordering, and cleanup. Your components handle how things look.
194+
195+
## Reactivity Model
196+
197+
Vue-dependent packages use Vue's reactivity primitives (`Ref`, `ComputedRef`, `readonly`) directly — no wrapper layer, no custom observable pattern:
198+
199+
```typescript
200+
const loading = createLoadingService();
201+
202+
loading.isLoading; // ComputedRef<boolean> — use in templates, watch, computed
203+
loading.activeCount; // DeepReadonly<Ref<number>> — readable but not writable
204+
```
205+
206+
This means services integrate naturally with Vue's ecosystem:
207+
208+
```vue
209+
<script setup lang="ts">
210+
import { watch } from "vue";
211+
import { loading } from "@/services";
212+
213+
// Standard Vue reactivity — nothing special
214+
watch(loading.isLoading, (isLoading) => {
215+
document.title = isLoading ? "Loading..." : "My App";
216+
});
217+
</script>
218+
```
219+
220+
### Why not Pinia?
221+
222+
Pinia is a global state management solution. These packages are **service factories** — they create isolated instances with their own encapsulated state. The difference matters:
223+
224+
- **Pinia store:** One global instance per store definition. Great for app-wide state.
225+
- **Service factory:** Create as many instances as needed. Great for domain-scoped state and testability.
226+
227+
`fs-adapter-store` is the bridge — it provides the reactive state management pattern (like Pinia) but as composable, per-domain instances (like a factory).
228+
229+
## Type Safety
230+
231+
TypeScript isn't just for autocomplete — it catches entire categories of bugs at compile time.
232+
233+
### Router Type Safety
234+
235+
`fs-router` extracts route names from your route definitions and validates navigation calls:
236+
237+
```typescript
238+
const routes = [
239+
createCrudRoutes("/users", "users", Layout, {
240+
overview: UsersList,
241+
create: UserCreate,
242+
edit: UserEdit,
243+
}),
244+
];
245+
246+
const router = createRouterService(routes);
247+
248+
router.goToEditPage("users", 42); // compiles — "users" exists and has an edit page
249+
router.goToEditPage("projects", 42); // compile error — "projects" is not a valid route
250+
```
251+
252+
### Translation Type Safety
253+
254+
`fs-translation` validates keys against your translation schema:
255+
256+
```typescript
257+
const translation = createTranslationService(
258+
{
259+
en: {
260+
common: { save: "Save", cancel: "Cancel" },
261+
users: { title: "Users", empty: "No users found" },
262+
},
263+
},
264+
"en",
265+
);
266+
267+
translation.t("common.save"); // compiles — "common.save" exists
268+
translation.t("common.delete"); // compile error — "common.delete" doesn't exist
269+
```
270+
271+
## The Dependency Graph
272+
273+
Packages form a directed graph with foundation packages at the bottom and domain packages at the top:
274+
275+
```
276+
┌──────────────┐ ┌────────────┐
277+
│ adapter-store│ │ router │
278+
└──┬──┬──┬──┬─┘ └────────────┘
279+
│ │ │ │
280+
┌──────────┘ │ │ └──────────┐
281+
│ │ │ │
282+
┌──────┴───┐ ┌─────┴──┴──┐ ┌───────┴─────┐
283+
│ helpers │ │ loading │ │ storage │
284+
└──────────┘ └─────┬─────┘ └─────────────┘
285+
286+
┌─────┴─────┐
287+
│ http │
288+
└───────────┘
289+
290+
┌────────┐ ┌────────┐ ┌─────────────┐ ┌───────┐
291+
│ theme │ │ toast │ │ translation │ │dialog │
292+
└────────┘ └────────┘ └─────────────┘ └───────┘
293+
```
294+
295+
- **Foundation packages** (http, storage, helpers) have no Vue dependency
296+
- **Service packages** (theme, loading, toast, dialog, translation) depend on Vue and optionally on foundation packages
297+
- **Domain packages** (adapter-store, router) compose multiple services into higher-level patterns
298+
- **Cross-cutting:** theme uses structural typing to accept a storage-shaped object without importing fs-storage

0 commit comments

Comments
 (0)