@noego/forge 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,99 +22,85 @@ You get **fast first paint & SEO** (SSR) *and* **instant page transitions** (cli
22
22
  - **Data Loading**: Unified mechanism for data fetching in SSR and client-side
23
23
  - **Vite Integration**: Modern, fast development with hot module reloading
24
24
 
25
- ## Quick Start
25
+ ## Getting Started
26
26
 
27
- ### TL;DR – Run the demo in two commands 🚀
27
+ ### Prerequisites
28
28
 
29
- ```bash
30
- # 1. Install dependencies (Forge + peers)
31
- npm install # or pnpm / yarn
29
+ Forge requires:
30
+ - **Node.js**: 16.x or higher
31
+ - **Express**: ^5.1.0 (peer dependency)
32
+ - **Svelte**: ^5.28.2 (peer dependency)
32
33
 
33
- # 2. Launch the development server (Express *and* Vite)
34
- npm run dev # -> http://localhost:3000
35
- ```
34
+ For development:
35
+ - **Vite**: ^6.3.5 (recommended for hot-module reloading and builds)
36
+ - **tsx**: ^4.19.3 (recommended for running TypeScript entry files)
36
37
 
37
- Vite is wired up as **middleware** inside Express (`middlewareMode: true`).
38
- That means hot-module-reloading, static files, and SSR all live behind the
39
- same port – you only ever need to open http://localhost:3000.
38
+ The versions shown in the badge at the very top are the ones Forge is tested against. Newer minor/patch releases of Express and Svelte usually work fine as well.
40
39
 
41
40
  ### Installation
42
41
 
42
+ Install Forge and its peer dependencies:
43
+
43
44
  ```bash
44
- npm i @noego/forge express svelte
45
- ```
45
+ # npm
46
+ npm install @noego/forge express@^5 svelte@^5
46
47
 
47
- Forge is a thin wrapper around **Express** and **Svelte**. The command above installs all three runtime dependencies you need. For development, you'll also want Vite for hot-module-reloading:
48
+ # yarn
49
+ yarn add @noego/forge express@^5 svelte@^5
48
50
 
49
- ```bash
50
- # development-only (HMR, build, …)
51
- npm i -D vite
51
+ # pnpm
52
+ pnpm add @noego/forge express@^5 svelte@^5
52
53
  ```
53
54
 
54
- The versions shown in the badge at the very top are always the ones that Forge
55
- is tested against; newer minor / patch releases of Express and Svelte usually
56
- work fine as well.
55
+ Then add development dependencies:
57
56
 
58
- ### Basic Setup
59
-
60
- ## Example Project Structure
57
+ ```bash
58
+ # npm
59
+ npm install --save-dev vite@^6 tsx @sveltejs/vite-plugin-svelte
61
60
 
62
- Forge is completely agnostic about where you place your files – as long as the
63
- paths in your **options object** are correct everything will “just work”. To make the
64
- following snippets less abstract, here is one possible layout for a **full
65
- stack** repository:
61
+ # yarn
62
+ yarn add --dev vite@^6 tsx @sveltejs/vite-plugin-svelte
66
63
 
67
- ```
68
- .
69
- ├─ frontend/ # Svelte + Forge source code
70
- │ ├─ components/
71
- │ ├─ openapi.yaml
72
- │ ├─ client.ts # client hydration entry
73
- │ └─ server.ts # Express SSR entry
74
- ├─ backend/ # (optional) REST / database code
75
- ├─ vite.config.js # shared by both dev & prod builds
76
- ├─ forge.config.ts # (optional) central Forge options shared by client & server
77
- └─ package.json # root-level scripts & deps
64
+ # pnpm
65
+ pnpm add --save-dev vite@^6 tsx @sveltejs/vite-plugin-svelte
78
66
  ```
79
67
 
80
- Feel free to flatten or reorganise folders – if you move `components/` for
81
- instance, simply update `component_dir` and the import paths keep resolving
82
- correctly.
68
+ ### Quickstart
83
69
 
84
- ### How Forge resolves paths
70
+ Here's the minimal setup to get a working Forge application running:
85
71
 
86
- Forge only really needs three absolute paths to bootstrap your app:
72
+ #### 1. Create your Express server (`server.ts`)
87
73
 
88
- 1. **`component_dir`** – look here for Svelte files referenced in the OpenAPI
89
- schema (every `x-view` / `x-layout` is interpreted *relative* to this
90
- directory).
91
- 2. **`open_api_path`** – the location of your `openapi.yaml`.
92
- 3. **`renderer`** – an HTML template that contains the placeholders
93
- `{{{APP}}}`, `{{{HEAD}}}` and `{{{DATA}}}`.
74
+ ```ts
75
+ import express from 'express';
76
+ import { createServer } from '@noego/forge/server';
94
77
 
95
- All paths are resolved against **`process.cwd()`** (your *project root*):
78
+ const app = express();
96
79
 
97
- * **Relative strings** – the preferred form. Pass `frontend/components` instead
98
- of `./frontend/components` so that the value still resolves correctly when
99
- your working directory changes (for instance when running tests).
100
- * **Absolute strings starting with `/`** – treated as root-relative, e.g.
101
- `/frontend/components` ➜ `path.join(process.cwd(), 'frontend/components')`.
80
+ const options = {
81
+ component_dir: 'components',
82
+ renderer: 'index.html',
83
+ open_api_path: 'openapi.yaml',
84
+ };
102
85
 
103
- Avoid the `./` prefix – while it works most of the time it breaks as soon as you
104
- invoke Node from a different folder.
86
+ await createServer(app, options);
105
87
 
106
- 1. **Configure OpenAPI Schema**
88
+ export default app;
89
+ ```
107
90
 
108
- Forge extends the OpenAPI 3 spec with a couple of `x-*` vendor extensions. Two
109
- conventions are important from day one:
91
+ #### 2. Create an entry file (`dev.ts`)
110
92
 
111
- * **Landing page must be explicit** – declare the `/` path manually, Forge will
112
- not create a default route for you.
113
- * **`x-layout` is always an array** – even when there is only one layout
114
- component. That keeps the type consistent and makes it possible to add more
115
- wrappers later without touching already deployed code.
93
+ ```ts
94
+ import app from './server';
116
95
 
117
- Create `openapi.yaml` in your project root:
96
+ const PORT = process.env.PORT || 3000;
97
+
98
+ app.listen(PORT, () => {
99
+ console.log(`🚀 Listening at http://localhost:${PORT}`);
100
+ });
101
+ ```
102
+
103
+ #### 3. Create your OpenAPI spec (`openapi.yaml`)
118
104
 
119
105
  ```yaml
120
106
  openapi: '3.0.3'
@@ -122,88 +108,48 @@ info:
122
108
  title: My App
123
109
  version: '1.0.0'
124
110
 
125
- x-fallback-view: error/404.svelte # 404 page
111
+ x-fallback-view: error/404.svelte
126
112
 
127
113
  paths:
128
114
  /:
129
115
  get:
130
116
  summary: Home page
131
117
  x-view: views/home.svelte
132
- x-layout:
133
- - layout/main.svelte
134
-
135
- '/user/:id':
136
- get:
137
- summary: User profile
138
- x-view: views/user_page.svelte
139
- x-layout:
118
+ x-layout:
140
119
  - layout/main.svelte
141
120
  ```
142
121
 
143
- 2. **Build (but don’t start) the Express server**
144
-
145
- Keeping server construction and `listen()` separate makes automated testing a
146
- lot easier because you can import the app without opening a TCP port.
147
-
148
- ```ts
149
- // server.ts – returns a fully configured Express application
150
- import express from 'express';
151
- import { createServer } from '@noego/forge/server';
152
- import { options } from './forge.config';
153
-
154
- export async function buildServer() {
155
- const app = express();
156
- await createServer(app, options);
157
- return app;
158
- }
159
- ```
160
-
161
- 3. **Start the server in dev / prod scripts**
162
-
163
- Your actual entry file is now a tiny wrapper that calls `buildServer()` and then
164
- `listen()`. Swap it out for a different bootstrapper in tests.
165
-
166
- ```ts
167
- // dev.ts – used by `npm run dev`
168
- import { buildServer } from './server';
169
-
170
- const PORT = process.env.PORT ?? 3000;
122
+ #### 4. Create components
171
123
 
172
- buildServer()
173
- .then(app => app.listen(PORT))
174
- .then(() => console.log(`🚀 http://localhost:${PORT}`))
175
- .catch(err => {
176
- console.error('Server failed to start', err);
177
- process.exit(1);
178
- });
179
- ```
124
+ ```html
125
+ <!-- components/layout/main.svelte -->
126
+ <script>
127
+ let { children } = $props();
128
+ </script>
180
129
 
181
- 4. **Create Client Initialization File**
130
+ <nav>
131
+ <a href="/">Home</a>
132
+ </nav>
133
+ <main>
134
+ {@render children()}
135
+ </main>
182
136
 
183
- ```typescript
184
- // client.ts
185
- import { createApp } from '@noego/forge/client';
186
- import { options } from './forge.config';
137
+ <!-- components/views/home.svelte -->
138
+ <h1>Welcome to Forge!</h1>
187
139
 
188
- document.addEventListener('DOMContentLoaded', () => {
189
- createApp(document.getElementById('app'), options);
190
- });
140
+ <!-- components/error/404.svelte -->
141
+ <h1>Page not found</h1>
191
142
  ```
192
143
 
193
- 5. **Create HTML Template**
144
+ #### 5. Create HTML template (`index.html`)
194
145
 
195
146
  ```html
196
- <!-- index.html -->
197
147
  <!DOCTYPE html>
198
148
  <html lang="en">
199
149
  <head>
200
150
  <meta charset="UTF-8">
201
151
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
152
  <title>My Forge App</title>
203
- <!-- 👇👇 **Important for Vite HMR during development** 👇👇 -->
204
- <!-- This script connects the page to Vite's Hot-Module-Reloading client. -->
205
- <!-- It MUST be included before your own client bundle when running the dev server. -->
206
- <!-- Vite HMR client (served from project root) -->
207
153
  <script type="module" src="/@vite/client"></script>
208
154
  <style>{{{CSS}}}</style>
209
155
  {{{HEAD}}}
@@ -211,309 +157,361 @@ document.addEventListener('DOMContentLoaded', () => {
211
157
  <body>
212
158
  <div id="app">{{{APP}}}</div>
213
159
  <script>window.__INITIAL_DATA__ = {{{DATA}}};</script>
214
- <!-- Application client entry (served from Vite dev server root) -->
215
160
  <script type="module" src="/client.ts"></script>
216
161
  </body>
217
162
  </html>
218
163
  ```
219
164
 
220
- 6. **Provide Forge Options** (file name & language are totally up to you)
165
+ #### 6. Create client entry (`client.ts`)
221
166
 
222
167
  ```ts
223
- // forge.config.ts (could also be .js / .mjs – only the exported object matters)
224
- import type { ServerOptions } from '@noego/forge/options';
168
+ import { createApp } from '@noego/forge/client';
225
169
 
226
- export const options: ServerOptions = {
170
+ const options = {
227
171
  component_dir: 'components',
228
172
  renderer: 'index.html',
229
- assets: {
230
- '/assets': ['public']
231
- },
232
173
  open_api_path: 'openapi.yaml',
233
- // Optional: directory containing server-side x-middleware modules
234
- middleware_path: 'middleware'
235
174
  };
236
- ```
237
-
238
- > See `docs/server_middleware.md` for more on declaring `x-middleware` chains.
239
-
240
- 7. **Create Components**
241
175
 
242
- ```html
243
- <!-- components/layout/main.svelte -->
244
- <script>
245
- let { children } = $props();
246
- </script>
247
-
248
- <nav>
249
- <a href="/">Home</a>
250
- <a href="/user/1">User</a>
251
- </nav>
252
- <main>
253
- {@render children()}
254
- </main>
255
-
256
- <!-- components/views/home.svelte -->
257
- <h1>Welcome to my app!</h1>
176
+ document.addEventListener('DOMContentLoaded', () => {
177
+ createApp(document.getElementById('app'), options);
178
+ });
179
+ ```
258
180
 
259
- <!-- components/views/user_page.svelte -->
260
- <script>
261
- export let params = {};
262
- </script>
181
+ #### 7. Add Vite config (`vite.config.js`)
263
182
 
264
- <h1>User {params.id}</h1>
183
+ ```js
184
+ import { defineConfig } from 'vite';
185
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
265
186
 
266
- <!-- components/error/404.svelte -->
267
- <h1>Page not found</h1>
187
+ export default defineConfig({
188
+ plugins: [svelte()],
189
+ });
268
190
  ```
269
191
 
270
- 8. **Start Development Server**
192
+ #### 8. Add npm scripts to `package.json`
271
193
 
272
- Add to your package.json:
273
194
  ```json
274
- "scripts": {
275
- "dev": "tsx dev.ts",
276
- "watch": "tsx watch --ignore '**/*.svelte' dev.ts",
277
- "typecheck": "tsc --noEmit && svelte-check"
195
+ {
196
+ "scripts": {
197
+ "dev": "tsx dev.ts",
198
+ "watch": "tsx watch --ignore '**/*.svelte' dev.ts",
199
+ "build:client": "vite build",
200
+ "build:ssr": "SSR=true vite build --ssr",
201
+ "build": "npm run build:client && npm run build:ssr",
202
+ "typecheck": "tsc --noEmit && svelte-check"
203
+ }
278
204
  }
279
205
  ```
280
206
 
281
- Run the server:
207
+ #### 9. Run the development server
208
+
282
209
  ```bash
283
210
  npm run dev
284
211
  ```
285
212
 
286
- ## Available npm scripts
213
+ Visit `http://localhost:3000` in your browser. Vite is running as middleware inside Express, so hot-module reloading (HMR) and SSR both live behind the same port.
287
214
 
288
- | Script | What it does |
289
- |--------|--------------|
290
- | `dev` | Express + Vite with HMR (default development mode) |
291
- | `watch` | Restarts Express when server-side files change |
292
- | `build` | Generates both client & SSR bundles (`vite build` twice) |
293
- | `typecheck` | Runs `tsc` and `svelte-check` |
215
+ ### Example Project Structure
294
216
 
295
- ## Further documentation 📚
217
+ Here's a recommended full-stack layout:
296
218
 
297
- The README is only the quick-start. For deeper topics have a look at the
298
- markdown files under [`/docs`](./docs):
219
+ ```
220
+ .
221
+ ├─ components/ # Svelte components
222
+ │ ├─ layout/
223
+ │ ├─ views/
224
+ │ └─ error/
225
+ ├─ openapi.yaml # OpenAPI specification
226
+ ├─ index.html # HTML template
227
+ ├─ client.ts # Client entry point
228
+ ├─ server.ts # Server entry point
229
+ ├─ dev.ts # Development entry
230
+ ├─ vite.config.js # Vite configuration
231
+ ├─ package.json
232
+ └─ tsconfig.json
233
+ ```
299
234
 
300
- | Document | What you will find |
301
- |----------|--------------------|
302
- | **layout_system.md** | How Forge composes nested layouts and views, best-practices, common pitfalls, and design guidelines. |
303
- | **load_functions.md** | How to fetch data safely on the server with `load()`, have it ready for SSR and client-side navigation, and access it via `$props()`. |
304
- | **state_sharing.md** | Pattern for reactive global state with Svelte 5 context and `$state`, including multi-property examples. |
305
- | **tailwind-layouts.md** | Design layouts with Tailwind CSS: global shells, nested wrappers, responsive tips. |
306
- | **FETCH_MIDDLEWARE.md** | Client-side fetch middleware: header injection, redirect handling, debugging helpers, security notes. |
235
+ Feel free to organize files differently the important thing is that paths in your options object point to the correct locations.
307
236
 
308
- More docs will be added over time—keep an eye on the folder when upgrading.
237
+ ## Usage
309
238
 
310
- 9. **Create a `vite.config.js` (if you don't already have one)**
239
+ ### Defining Routes with OpenAPI
311
240
 
312
- Forge relies on Vite for both development (HMR) and production builds.
313
- If your project was scaffolded from scratch you will need to add a minimal
314
- `vite.config.js` file at the root of the repository so Vite knows how to handle
315
- Svelte files:
241
+ Forge extends OpenAPI 3 with custom `x-*` vendor extensions to define your application structure. Every route in your application is declared in `openapi.yaml`:
316
242
 
317
- ```js
318
- // vite.config.js
319
- import { defineConfig } from 'vite';
320
- import { svelte } from '@sveltejs/vite-plugin-svelte';
243
+ #### Key OpenAPI Extensions
321
244
 
322
- export default defineConfig({
323
- plugins: [svelte()],
324
- });
245
+ | Extension | Description |
246
+ |-----------|-------------|
247
+ | `x-view` | Path to the Svelte component that renders the page view |
248
+ | `x-layout` | Array of layout components (rendered outside-in) that wrap the view |
249
+ | `x-fallback-view` | Component to render for 404 errors |
250
+ | `x-fallback-layout` | Default layout when none is specified |
251
+ | `x-middleware` | Array of middleware functions to run before rendering |
252
+
253
+ #### Important Conventions
254
+
255
+ 1. **Landing page must be explicit** – Always declare the `/` path manually. Forge will not create a default route for you.
256
+ 2. **`x-layout` is always an array** – Even with one layout component. This keeps types consistent and lets you add more wrappers later without breaking deployed code.
257
+
258
+ #### Example OpenAPI Schema
259
+
260
+ ```yaml
261
+ openapi: '3.0.3'
262
+ info:
263
+ title: My App
264
+ version: '1.0.0'
265
+
266
+ x-fallback-view: error/404.svelte
267
+
268
+ paths:
269
+ /:
270
+ get:
271
+ summary: Home page
272
+ x-view: views/home.svelte
273
+ x-layout:
274
+ - layout/main.svelte
275
+
276
+ '/user/:id':
277
+ get:
278
+ summary: User profile
279
+ x-view: views/user_page.svelte
280
+ x-layout:
281
+ - layout/main.svelte
282
+
283
+ '/products':
284
+ get:
285
+ summary: Product listing
286
+ x-view: views/products.svelte
287
+ x-layout:
288
+ - layout/main.svelte
289
+ - layout/products_layout.svelte
325
290
  ```
326
291
 
327
- This can of course be extended with any additional Vite options that your
328
- application requires – the only strict requirement is that the Svelte plugin is
329
- present so that `*.svelte` files are compiled correctly.
292
+ ### Path Resolution
330
293
 
331
- ## Configuration Options
294
+ Forge resolves paths relative to `process.cwd()` (your project root). Use one of these formats:
332
295
 
333
- ### Server Options
296
+ **Relative paths (recommended):**
297
+ ```ts
298
+ const options = {
299
+ component_dir: 'components', // ✅ Correct
300
+ renderer: 'index.html',
301
+ open_api_path: 'openapi.yaml',
302
+ };
303
+ ```
334
304
 
335
- | Option | Type | Default | Description |
336
- |--------|------|---------|-------------|
337
- | `development` | boolean | `process.env.NODE_ENV !== 'production'` | Development mode flag |
338
- | `component_dir` | string | `/` | Directory containing Svelte components |
339
- | `build_dir` | string | `dist_ssr` | Output directory for production builds |
340
- | `renderer` | string \| IHTMLRender | `default` | HTML template path or renderer |
341
- | `open_api_path` | string | `process.cwd() + '/openapi.yaml'` | Path to OpenAPI specification |
342
- | `manifest_endpoint` | string | `/manifest.json` | Endpoint for manifest file |
343
- | `assets` | Record<string, string[]> | `undefined` | Map of URL paths to directories for static assets |
344
- | `viteOptions` | object | See code | Vite configuration options |
305
+ **Root-relative paths (absolute in the project root):**
306
+ ```ts
307
+ const options = {
308
+ component_dir: '/frontend/components', // Also correct
309
+ renderer: '/frontend/index.html',
310
+ };
311
+ ```
345
312
 
346
- ### OpenAPI Extensions
313
+ **Avoid relative paths with `./` prefix:**
314
+ ```ts
315
+ const options = {
316
+ component_dir: './components', // ❌ Breaks when Node is invoked from different directories
317
+ };
318
+ ```
347
319
 
348
- Forge uses custom OpenAPI extensions to define your application structure:
320
+ ### HTML Template Placeholders
349
321
 
350
- | Extension | Description |
351
- |-----------|-------------|
352
- | `x-view` | Path to the Svelte component that handles the route |
353
- | `x-layout` | Array of layout components that wrap the view (rendered outside-in) |
354
- | `x-fallback-view` | Component to render for 404 errors |
355
- | `x-fallback-layout` | Default layout when none is specified |
322
+ Your renderer (typically `index.html`) must contain these three placeholders:
356
323
 
357
- ## Advanced Usage
324
+ | Placeholder | Content |
325
+ |-------------|---------|
326
+ | `{{{APP}}}` | Server-rendered HTML from your Svelte components |
327
+ | `{{{HEAD}}}` | Head tags generated by Svelte (meta, stylesheets, etc.) |
328
+ | `{{{DATA}}}` | Initial data from `load()` functions, serialized as JSON |
358
329
 
359
- ### Fetch Middleware for Authentication
330
+ ### Configuration Options
360
331
 
361
- Forge provides a client-side fetch middleware system that allows you to automatically modify all fetch requests, eliminating the need to manually add authentication headers to every API call.
332
+ Create a central options file (typically `forge.config.ts` or `forge.config.js`):
362
333
 
363
- ```typescript
364
- // client.ts
365
- import { createApp, fetch } from '@noego/forge/client';
366
- import { options } from './forge.config';
334
+ ```ts
335
+ import type { ServerOptions } from '@noego/forge/options';
367
336
 
368
- // Configure fetch middleware to automatically add authentication headers
369
- fetch.configUpdate((url, init) => {
370
- const token = localStorage.getItem('auth_token');
371
- if (token) {
372
- const headers = new Headers(init?.headers);
373
- headers.set('Authorization', `Bearer ${token}`);
374
- return {
375
- ...init,
376
- headers
377
- };
378
- }
379
- return init;
380
- });
337
+ export const options: ServerOptions = {
338
+ // Required
339
+ component_dir: 'components',
340
+ renderer: 'index.html',
341
+ open_api_path: 'openapi.yaml',
381
342
 
382
- document.addEventListener('DOMContentLoaded', () => {
383
- createApp(document.getElementById('app'), options);
384
- });
343
+ // Optional
344
+ development: process.env.NODE_ENV !== 'production',
345
+ build_dir: 'dist_ssr',
346
+ manifest_endpoint: '/manifest.json',
347
+ middleware_path: 'middleware',
348
+
349
+ // Static assets mapping
350
+ assets: {
351
+ '/assets': ['public'],
352
+ '/images': ['resources/images'],
353
+ }
354
+ };
385
355
  ```
386
356
 
387
- With this middleware configured, all fetch calls in your application will automatically include the Authorization header:
357
+ ### Component Data Loading
388
358
 
389
- ```typescript
390
- // No need to manually add Authorization headers anymore
391
- const response = await fetch('/api/contractor-types', {
392
- method: 'POST',
393
- headers: { 'Content-Type': 'application/json' },
394
- body: JSON.stringify({ name: 'New Type' })
395
- });
359
+ Components can export a server-only `load()` function to fetch and prepare data before rendering:
360
+
361
+ ```svelte
362
+ <!-- components/views/user.svelte -->
363
+ <script lang="ts">
364
+ export let data: any;
365
+
366
+ export async function load(request) {
367
+ const { params, query } = request;
368
+
369
+ // Fetch data from your API or database
370
+ const user = await fetchUser(params.id);
371
+
372
+ return {
373
+ user,
374
+ debug: { query }
375
+ };
376
+ }
377
+ </script>
378
+
379
+ <h1>{data.user.name}</h1>
396
380
  ```
397
381
 
398
- The middleware function receives the same parameters as the native `fetch()` function and should return the modified `RequestInit` object. You can use this pattern for:
382
+ The `load()` function receives a `RequestData` object with:
399
383
 
400
- - Adding authentication tokens
401
- - Setting default headers
402
- - Logging requests
403
- - Transforming request data
384
+ | Property | Description |
385
+ |----------|-------------|
386
+ | `url` | Full request URL |
387
+ | `params` | Dynamic route parameters (e.g., `{ id: '123' }`) |
388
+ | `query` | Query string parameters |
389
+ | `headers` | Request headers |
390
+ | `body` | Parsed request body (POST/PUT/etc.) |
391
+ | `context` | Per-request context bag (mutated by middleware) |
392
+
393
+ **Forge behavior:**
394
+ 1. During **SSR**: `load()` is called on the server, data is embedded in the HTML
395
+ 2. During **client-side navigation**: Forge automatically fetches the data as JSON from the same URL
396
+ 3. The component receives identical data in both cases
404
397
 
405
398
  ### Nested Layouts
406
399
 
407
- Forge supports nested layouts, rendered in the order specified:
400
+ Layouts are composed in the order specified, wrapping each other:
408
401
 
409
402
  ```yaml
410
403
  paths:
411
- '/product/:id':
404
+ '/admin/users':
412
405
  get:
413
- x-view: views/product_page.svelte # 👈 view component
406
+ x-view: views/admin/users_list.svelte
414
407
  x-layout:
415
- - layout/main.svelte # outer layout
416
- - layout/product_layout.svelte # inner (page-specific) layout
408
+ - layout/main.svelte # outer layout
409
+ - layout/admin_layout.svelte # middle layout
410
+ - layout/sidebar.svelte # inner layout
417
411
  ```
418
412
 
419
- The resulting component tree:
413
+ This creates the component tree:
420
414
  ```
421
415
  main.svelte
422
- └── product_layout.svelte
423
- └── product_page.svelte (view)
416
+ └── admin_layout.svelte
417
+ └── sidebar.svelte
418
+ └── users_list.svelte (view)
424
419
  ```
425
420
 
421
+ Each layout receives a `children()` snippet to render inner content.
422
+
426
423
  ### Static Assets
427
424
 
428
- Configure multiple asset directories:
425
+ Map URL paths to directories on disk:
429
426
 
430
- ```typescript
431
- assets: {
432
- '/assets': ['public'],
433
- '/images': ['resources/images'],
434
- '/docs': ['documentation']
435
- }
427
+ ```ts
428
+ const options = {
429
+ assets: {
430
+ '/assets': ['public'], // GET /assets/style.css → public/style.css
431
+ '/images': ['resources/images'], // GET /images/logo.png → resources/images/logo.png
432
+ }
433
+ };
436
434
  ```
437
435
 
438
- **Important:** every directory listed in `assets` *must* exist at server
439
- start-up, otherwise `express.static` will throw an error and Forge will refuse to
440
- boot. If you need to commit an empty folder simply add a dummy
441
- `public/.gitkeep` file so the directory makes it into version control.
436
+ **Important**: All directories in `assets` must exist when the server starts, otherwise Express will throw an error. Use `.gitkeep` files to commit empty directories.
442
437
 
443
- ### Component Data Loading
438
+ ### Client-Side Fetch Middleware
444
439
 
445
- Svelte components can export a **server-only** `load` function that returns the
446
- initial data for the page.
440
+ Automatically modify all fetch requests (e.g., add authentication headers):
447
441
 
448
- Forge will:
449
- 1. Invoke `load()` during SSR and embed the returned JSON into the HTML.
450
- 2. On subsequent **client-side navigations** automatically issue an
451
- `application/json` request to the same URL and pass the payload to the
452
- destination component – you don’t have to write any extra code.
442
+ ```ts
443
+ // client.ts
444
+ import { createApp, fetch } from '@noego/forge/client';
445
+ import { options } from './forge.config';
453
446
 
454
- ```html
455
- <script lang="ts">
456
- export let data: any; // data returned by `load()`
457
-
458
- /**
459
- * `load()` is executed exactly once on the server.
460
- * It receives a single argument – an object that describes the incoming
461
- * request. Forge then serialises the returned value and re-hydrates it on
462
- * the client (or performs an automatic JSON fetch when navigating
463
- * client-side).
464
- */
465
- export async function load(request) {
466
- const { params, query } = request;
467
- // Fetch data however you like (Database, internal service, etc.)
468
- return {
469
- username: `User #${params.id}`,
470
- debug: { query }
471
- };
447
+ // Add a global fetch middleware
448
+ fetch.configUpdate((url, init) => {
449
+ const token = localStorage.getItem('auth_token');
450
+ if (token) {
451
+ const headers = new Headers(init?.headers);
452
+ headers.set('Authorization', `Bearer ${token}`);
453
+ return { ...init, headers };
472
454
  }
473
- </script>
455
+ return init;
456
+ });
474
457
 
475
- <h1>{data.username}</h1>
458
+ document.addEventListener('DOMContentLoaded', () => {
459
+ createApp(document.getElementById('app'), options);
460
+ });
476
461
  ```
477
462
 
478
- The `request` object passed to `load()` (alias: `RequestData`) contains:
463
+ Now all fetch calls automatically include the token:
479
464
 
480
- | Property | Description |
481
- |----------|-------------|
482
- | `url` | Full request URL. |
483
- | `params` | Dynamic route params parsed from the URL. |
484
- | `query` | Query-string parameters. |
485
- | `headers`| Raw request headers. |
486
- | `body` | Parsed request body (for POST/PUT …). |
487
- | `context`| Mutable per-request bag you can populate in middleware. |
465
+ ```ts
466
+ // No manual headers needed
467
+ const response = await fetch('/api/users', {
468
+ method: 'POST',
469
+ headers: { 'Content-Type': 'application/json' },
470
+ body: JSON.stringify({ name: 'Alice' })
471
+ });
472
+ ```
488
473
 
489
- ## Building for Production
474
+ ## Advanced Topics
490
475
 
491
- 1. **Add build scripts to package.json**:
476
+ ### Development vs. Production Behavior
492
477
 
493
- ```json
494
- "scripts": {
495
- "build:client": "vite build",
496
- "build:ssr": "SSR=true vite build --ssr",
497
- "build": "npm run build:client && npm run build:ssr"
498
- }
499
- ```
478
+ **Development mode** (`npm run dev`):
479
+ - Vite runs as middleware inside Express (`middlewareMode: true`)
480
+ - Hot Module Reloading (HMR) is enabled
481
+ - Svelte components are compiled on-the-fly
482
+ - All requests go through a single Express port
483
+
484
+ **Production mode** (`NODE_ENV=production`):
485
+ - Static pre-built client and SSR bundles are served
486
+ - No Vite middleware (faster, smaller footprint)
487
+ - Use `npm run build` to generate `dist/` and `dist_ssr/` directories
500
488
 
501
- 2. **Run build**:
489
+ ### Building for Production
490
+
491
+ 1. Generate production bundles:
502
492
 
503
493
  ```bash
504
494
  npm run build
505
495
  ```
506
496
 
507
- 3. **Start production server**:
497
+ This runs two Vite builds:
498
+ - **Client build**: Creates `dist/` for browser assets
499
+ - **SSR build**: Creates `dist_ssr/` for server bundle
500
+
501
+ 2. Serve in production:
508
502
 
509
503
  ```bash
510
504
  NODE_ENV=production node server.js
511
505
  ```
512
506
 
513
- ## Testing with Vitest / Jest
507
+ Your Forge app will serve:
508
+ - Pre-compiled client code from `dist/`
509
+ - Server-side rendered HTML using `dist_ssr/`
510
+ - No Vite runtime overhead
514
511
 
515
- When you test Forge apps with **Vitest** or **Jest** you usually drive the
516
- Express server via **supertest**:
512
+ ### Testing with Vitest / Jest
513
+
514
+ Use **supertest** to drive your Express server in tests:
517
515
 
518
516
  ```ts
519
517
  import request from 'supertest';
@@ -532,88 +530,148 @@ afterAll(() => {
532
530
  });
533
531
  ```
534
532
 
535
- Two things to remember:
533
+ **Key points:**
534
+ - HTML is streamed, so use `res.text` not `res.body`
535
+ - Always close the server after tests to release handles
536
+ - `buildServer()` doesn't call `listen()`, so you control the lifecycle
536
537
 
537
- 1. The **HTML is streamed**, so the response body is available in `res.text`.
538
- 2. Always close the server after long-running suites to release file & WS handles.
538
+ ### Server-Side Middleware
539
539
 
540
- ## Troubleshooting
540
+ Define custom middleware in `x-middleware` arrays to run before rendering:
541
541
 
542
- ### Common Issues
542
+ ```yaml
543
+ paths:
544
+ '/admin':
545
+ get:
546
+ x-middleware:
547
+ - requireAuth
548
+ - loadUserData
549
+ x-view: views/admin.svelte
550
+ ```
543
551
 
544
- #### Components Not Loading
552
+ Middleware files are loaded from your `middleware_path` and can modify the request context:
545
553
 
546
- Ensure your `component_dir` matches the directory structure and is accessible relative to your project root.
554
+ ```ts
555
+ // middleware/requireAuth.ts
556
+ export default (request) => {
557
+ if (!request.headers.authorization) {
558
+ throw new Error('Unauthorized');
559
+ }
560
+ request.context.user = parseToken(request.headers.authorization);
561
+ };
562
+ ```
547
563
 
548
- #### SSR Hydration Errors
564
+ See `docs/server_middleware.md` for detailed examples.
549
565
 
550
- - Check for components that use browser-specific APIs without checking for `typeof window !== 'undefined'`
551
- - Ensure components use the same data during SSR and client-side hydration
566
+ ### Stitch Integration (Modular OpenAPI)
552
567
 
553
- #### HMR not triggering inside WSL / Docker
568
+ For large projects, split your OpenAPI spec into multiple files with **Stitch**:
554
569
 
555
- File system events can behave differently inside virtualised environments. If
556
- you do not see hot-reloading updates, set `watch: { usePolling: true }` in
557
- `vite.config.js`. This project’s config already enables it – the note is here
558
- so you know *why*.
570
+ 1. Create `stitch.yaml`:
559
571
 
560
- #### "Unexpected token <" / Hydration mismatch warnings
572
+ ```yaml
573
+ stitch:
574
+ - openapi/base.yaml
575
+ - openapi/routes/*.yaml
576
+ - openapi/components/*.yaml
577
+ ```
561
578
 
562
- Usually indicates that the HTML the server sent does not match the DOM the
563
- client tries to hydrate. Double-check that any data returned from a
564
- component’s `load()` function is identical on **both** sides.
579
+ 2. In your client, load YAML via `import.meta.glob()`:
565
580
 
566
- #### Route Not Found
581
+ ```ts
582
+ const yamlFiles = import.meta.glob('./openapi/**/*.yaml', {
583
+ query: '?raw',
584
+ import: 'default',
585
+ eager: true
586
+ });
567
587
 
568
- - Verify your OpenAPI paths are correctly defined
569
- - Check that component file paths in `x-view` and `x-layout` are correct
570
- - Ensure all referenced components exist
588
+ await createApp(document.getElementById('app'), {
589
+ open_api_path: 'openapi/stitch.yaml',
590
+ component_dir: 'components'
591
+ });
592
+ ```
571
593
 
572
- ## Stitch Integration (Modular YAML)
594
+ Forge automatically detects stitch files and merges them into a single spec. No build step required – hot reloading works automatically.
573
595
 
574
- Forge automatically detects and builds stitch configurations for modular OpenAPI development. Instead of maintaining a single large `openapi.yaml`, you can split your API specification into multiple files.
596
+ ### Hydration and SSR Debugging
575
597
 
576
- ### Using Stitch Files
598
+ **Common hydration mismatch issues:**
599
+ - Component uses browser-only APIs (e.g., `window`, `document`) without checking `typeof window`
600
+ - Different data is returned by `load()` on server vs. client
601
+ - Non-deterministic data (dates, random numbers, etc.)
577
602
 
578
- 1. **Create a stitch configuration** (`stitch.yaml`):
603
+ **Debug tips:**
604
+ ```ts
605
+ // ✅ Safe: check for browser environment
606
+ export async function load(request) {
607
+ if (typeof window !== 'undefined') {
608
+ // Client-only code
609
+ }
610
+ return data;
611
+ }
579
612
 
580
- ```yaml
581
- stitch:
582
- - openapi/base.yaml
583
- - openapi/routes/*.yaml
584
- - openapi/components/*.yaml
613
+ // ✅ Deterministic: same data every time
614
+ const data = {
615
+ timestamp: '2025-10-22T00:00:00Z', // Fixed, not Date.now()
616
+ userId: params.id,
617
+ };
618
+
619
+ // ❌ Unsafe: different every render
620
+ const data = { id: Math.random() };
585
621
  ```
586
622
 
587
- 2. **Pre-load YAML modules** in your client application:
623
+ ### Additional Documentation
588
624
 
589
- ```typescript
590
- // Client-side: Load YAML files into Vite's module cache
591
- const yamlFiles = import.meta.glob('./openapi/**/*.yaml', { query: '?raw', import: 'default', eager: true });
625
+ For deeper topics, check the documentation in [`/docs`](./docs):
592
626
 
593
- await createApp(document.getElementById('app'), {
594
- open_api_path: 'frontend/stitch.yaml', // Framework auto-detects and builds
595
- component_dir: 'frontend/components/views'
627
+ | Document | Coverage |
628
+ |----------|----------|
629
+ | **layout_system.md** | Composing nested layouts, best practices, common pitfalls |
630
+ | **load_functions.md** | Data fetching with `load()`, SSR and client-side behavior |
631
+ | **state_sharing.md** | Reactive global state with Svelte 5 context and `$state` |
632
+ | **tailwind-layouts.md** | Design layouts with Tailwind CSS |
633
+ | **FETCH_MIDDLEWARE.md** | Client-side fetch middleware, authentication, security |
634
+ | **server_middleware.md** | Per-route server middleware and request context |
635
+
636
+
637
+ ## Troubleshooting
638
+
639
+ ### Common Issues
640
+
641
+ **Components Not Loading**
642
+
643
+ Ensure your `component_dir` matches the directory structure and is accessible relative to your project root. Verify paths are resolved correctly against `process.cwd()`.
644
+
645
+ **SSR Hydration Errors**
646
+
647
+ Common causes:
648
+ - Components use browser-only APIs (`window`, `document`) without checking `typeof window !== 'undefined'`
649
+ - Component returns different data during SSR vs. client-side navigation
650
+ - Non-deterministic data (random values, current timestamps)
651
+
652
+ **HMR not triggering inside WSL / Docker**
653
+
654
+ File system events behave differently in virtualized environments. Add this to `vite.config.js`:
655
+
656
+ ```js
657
+ export default defineConfig({
658
+ plugins: [svelte()],
659
+ watch: { usePolling: true }
596
660
  });
597
661
  ```
598
662
 
599
- 3. **Framework auto-detection** Forge automatically:
600
- - Detects stitch files by checking for the `stitch` root property
601
- - Builds the final OpenAPI configuration using `@noego/stitch/browser`
602
- - Uses the cached YAML modules from your `import.meta.glob` call
603
- - Falls back to regular OpenAPI processing for non-stitch files
604
-
605
- ### Benefits
663
+ **"Unexpected token <" / Hydration mismatch warnings**
606
664
 
607
- - **No manual build steps** No need for `npm run stitch:frontend`
608
- - **Modular development** Split complex APIs into focused files
609
- - **Hot reloading** Vite automatically reloads when YAML files change
610
- - **Backwards compatible** – Existing `openapi.yaml` files work unchanged
665
+ The HTML from the server doesn't match the DOM the client tries to hydrate. Ensure:
666
+ - Component `load()` functions return identical data on server and client
667
+ - No random values or time-dependent logic in initial render
611
668
 
612
- ### Requirements
669
+ **Route Not Found**
613
670
 
614
- - Client applications must call `import.meta.glob('./openapi/**/*.yaml', ...)` for Vite caching
615
- - The glob pattern must be **static** and **top-level** for Vite's static analysis
616
- - YAML files are loaded into the browser bundle but only once (no duplication)
671
+ Check:
672
+ 1. OpenAPI paths are correctly defined in `openapi.yaml`
673
+ 2. Component file paths in `x-view` and `x-layout` exist and are relative to `component_dir`
674
+ 3. All referenced component files exist on disk
617
675
 
618
676
  ## Contributing
619
677