@noego/forge 0.0.9 → 0.0.11
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 +441 -383
- package/dist/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.ts +16 -2
- package/dist/client.mjs +261 -244
- package/dist/client.mjs.map +1 -1
- package/dist/page.svelte-Bq1Q01H0.js.map +1 -1
- package/dist/page.svelte-Dvj7306U.cjs.map +1 -1
- package/dist-ssr/{path-9twSsimy.js → path-BqcF5dbs.js} +3 -2
- package/dist-ssr/{path-9twSsimy.js.map → path-BqcF5dbs.js.map} +1 -1
- package/dist-ssr/{path-Dm_4PXDW.cjs → path-sxXxpB6R.cjs} +3 -2
- package/dist-ssr/{path-Dm_4PXDW.cjs.map → path-sxXxpB6R.cjs.map} +1 -1
- package/dist-ssr/server.cjs +24 -7
- package/dist-ssr/server.cjs.map +1 -1
- package/dist-ssr/server.js +24 -7
- package/dist-ssr/server.js.map +1 -1
- package/dist-ssr/shared.cjs +1 -1
- package/dist-ssr/shared.js +1 -1
- package/package.json +1 -1
- package/schema.json +16 -6
- package/src/components/RecursiveRender.svelte +55 -14
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
|
-
##
|
|
25
|
+
## Getting Started
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### Prerequisites
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
45
|
+
# npm
|
|
46
|
+
npm install @noego/forge express@^5 svelte@^5
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
# yarn
|
|
49
|
+
yarn add @noego/forge express@^5 svelte@^5
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
npm i -D vite
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add @noego/forge express@^5 svelte@^5
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
```bash
|
|
58
|
+
# npm
|
|
59
|
+
npm install --save-dev vite@^6 tsx @sveltejs/vite-plugin-svelte
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
81
|
-
instance, simply update `component_dir` and the import paths keep resolving
|
|
82
|
-
correctly.
|
|
68
|
+
### Quickstart
|
|
83
69
|
|
|
84
|
-
|
|
70
|
+
Here's the minimal setup to get a working Forge application running:
|
|
85
71
|
|
|
86
|
-
|
|
72
|
+
#### 1. Create your Express server (`server.ts`)
|
|
87
73
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
78
|
+
const app = express();
|
|
96
79
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
80
|
+
const options = {
|
|
81
|
+
component_dir: 'components',
|
|
82
|
+
renderer: 'index.html',
|
|
83
|
+
open_api_path: 'openapi.yaml',
|
|
84
|
+
};
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
invoke Node from a different folder.
|
|
86
|
+
await createServer(app, options);
|
|
105
87
|
|
|
106
|
-
|
|
88
|
+
export default app;
|
|
89
|
+
```
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
conventions are important from day one:
|
|
91
|
+
#### 2. Create an entry file (`dev.ts`)
|
|
110
92
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
process.exit(1);
|
|
178
|
-
});
|
|
179
|
-
```
|
|
124
|
+
```html
|
|
125
|
+
<!-- components/layout/main.svelte -->
|
|
126
|
+
<script>
|
|
127
|
+
let { children } = $props();
|
|
128
|
+
</script>
|
|
180
129
|
|
|
181
|
-
|
|
130
|
+
<nav>
|
|
131
|
+
<a href="/">Home</a>
|
|
132
|
+
</nav>
|
|
133
|
+
<main>
|
|
134
|
+
{@render children()}
|
|
135
|
+
</main>
|
|
182
136
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
140
|
+
<!-- components/error/404.svelte -->
|
|
141
|
+
<h1>Page not found</h1>
|
|
191
142
|
```
|
|
192
143
|
|
|
193
|
-
5.
|
|
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.
|
|
165
|
+
#### 6. Create client entry (`client.ts`)
|
|
221
166
|
|
|
222
167
|
```ts
|
|
223
|
-
|
|
224
|
-
import type { ServerOptions } from '@noego/forge/options';
|
|
168
|
+
import { createApp } from '@noego/forge/client';
|
|
225
169
|
|
|
226
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
260
|
-
<script>
|
|
261
|
-
export let params = {};
|
|
262
|
-
</script>
|
|
181
|
+
#### 7. Add Vite config (`vite.config.js`)
|
|
263
182
|
|
|
264
|
-
|
|
183
|
+
```js
|
|
184
|
+
import { defineConfig } from 'vite';
|
|
185
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
265
186
|
|
|
266
|
-
|
|
267
|
-
|
|
187
|
+
export default defineConfig({
|
|
188
|
+
plugins: [svelte()],
|
|
189
|
+
});
|
|
268
190
|
```
|
|
269
191
|
|
|
270
|
-
8.
|
|
192
|
+
#### 8. Add npm scripts to `package.json`
|
|
271
193
|
|
|
272
|
-
Add to your package.json:
|
|
273
194
|
```json
|
|
274
|
-
|
|
275
|
-
"
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
Here's a recommended full-stack layout:
|
|
296
218
|
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
+
## Usage
|
|
309
238
|
|
|
310
|
-
|
|
239
|
+
### Defining Routes with OpenAPI
|
|
311
240
|
|
|
312
|
-
Forge
|
|
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
|
-
|
|
318
|
-
// vite.config.js
|
|
319
|
-
import { defineConfig } from 'vite';
|
|
320
|
-
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
243
|
+
#### Key OpenAPI Extensions
|
|
321
244
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
Forge resolves paths relative to `process.cwd()` (your project root). Use one of these formats:
|
|
332
295
|
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
+
### HTML Template Placeholders
|
|
349
321
|
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
330
|
+
### Configuration Options
|
|
360
331
|
|
|
361
|
-
|
|
332
|
+
Create a central options file (typically `forge.config.ts` or `forge.config.js`):
|
|
362
333
|
|
|
363
|
-
```
|
|
364
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
357
|
+
### Component Data Loading
|
|
388
358
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
382
|
+
The `load()` function receives a `RequestData` object with:
|
|
399
383
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
400
|
+
Layouts are composed in the order specified, wrapping each other:
|
|
408
401
|
|
|
409
402
|
```yaml
|
|
410
403
|
paths:
|
|
411
|
-
'/
|
|
404
|
+
'/admin/users':
|
|
412
405
|
get:
|
|
413
|
-
x-view: views/
|
|
406
|
+
x-view: views/admin/users_list.svelte
|
|
414
407
|
x-layout:
|
|
415
|
-
- layout/main.svelte
|
|
416
|
-
- 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
|
-
|
|
413
|
+
This creates the component tree:
|
|
420
414
|
```
|
|
421
415
|
main.svelte
|
|
422
|
-
└──
|
|
423
|
-
└──
|
|
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
|
-
|
|
425
|
+
Map URL paths to directories on disk:
|
|
429
426
|
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
438
|
+
### Client-Side Fetch Middleware
|
|
444
439
|
|
|
445
|
-
|
|
446
|
-
initial data for the page.
|
|
440
|
+
Automatically modify all fetch requests (e.g., add authentication headers):
|
|
447
441
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
455
|
+
return init;
|
|
456
|
+
});
|
|
474
457
|
|
|
475
|
-
|
|
458
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
459
|
+
createApp(document.getElementById('app'), options);
|
|
460
|
+
});
|
|
476
461
|
```
|
|
477
462
|
|
|
478
|
-
|
|
463
|
+
Now all fetch calls automatically include the token:
|
|
479
464
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
##
|
|
474
|
+
## Advanced Topics
|
|
490
475
|
|
|
491
|
-
|
|
476
|
+
### Development vs. Production Behavior
|
|
492
477
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
538
|
-
2. Always close the server after long-running suites to release file & WS handles.
|
|
538
|
+
### Server-Side Middleware
|
|
539
539
|
|
|
540
|
-
|
|
540
|
+
Define custom middleware in `x-middleware` arrays to run before rendering:
|
|
541
541
|
|
|
542
|
-
|
|
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
|
-
|
|
552
|
+
Middleware files are loaded from your `middleware_path` and can modify the request context:
|
|
545
553
|
|
|
546
|
-
|
|
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
|
-
|
|
564
|
+
See `docs/server_middleware.md` for detailed examples.
|
|
549
565
|
|
|
550
|
-
|
|
551
|
-
- Ensure components use the same data during SSR and client-side hydration
|
|
566
|
+
### Stitch Integration (Modular OpenAPI)
|
|
552
567
|
|
|
553
|
-
|
|
568
|
+
For large projects, split your OpenAPI spec into multiple files with **Stitch**:
|
|
554
569
|
|
|
555
|
-
|
|
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
|
-
|
|
572
|
+
```yaml
|
|
573
|
+
stitch:
|
|
574
|
+
- openapi/base.yaml
|
|
575
|
+
- openapi/routes/*.yaml
|
|
576
|
+
- openapi/components/*.yaml
|
|
577
|
+
```
|
|
561
578
|
|
|
562
|
-
|
|
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
|
-
|
|
581
|
+
```ts
|
|
582
|
+
const yamlFiles = import.meta.glob('./openapi/**/*.yaml', {
|
|
583
|
+
query: '?raw',
|
|
584
|
+
import: 'default',
|
|
585
|
+
eager: true
|
|
586
|
+
});
|
|
567
587
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
588
|
+
await createApp(document.getElementById('app'), {
|
|
589
|
+
open_api_path: 'openapi/stitch.yaml',
|
|
590
|
+
component_dir: 'components'
|
|
591
|
+
});
|
|
592
|
+
```
|
|
571
593
|
|
|
572
|
-
|
|
594
|
+
Forge automatically detects stitch files and merges them into a single spec. No build step required – hot reloading works automatically.
|
|
573
595
|
|
|
574
|
-
|
|
596
|
+
### Hydration and SSR Debugging
|
|
575
597
|
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
623
|
+
### Additional Documentation
|
|
588
624
|
|
|
589
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
-
|
|
609
|
-
-
|
|
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
|
-
|
|
669
|
+
**Route Not Found**
|
|
613
670
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|