@inglorious/ssx 0.4.1 → 1.1.0
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 +444 -662
- package/bin/ssx.js +17 -21
- package/package.json +2 -7
- package/src/{build.test.js → build/build.test.js} +1 -1
- package/src/build/index.js +161 -0
- package/src/build/manifest.js +112 -0
- package/src/build/metadata.js +53 -0
- package/src/build/pages.js +40 -0
- package/src/build/public.js +34 -0
- package/src/build/rss.js +102 -0
- package/src/build/sitemap.js +57 -0
- package/src/build/vite-config.js +51 -0
- package/src/config.js +16 -0
- package/src/{dev.js → dev/index.js} +32 -35
- package/src/dev/vite-config.js +40 -0
- package/src/module.js +0 -3
- package/src/page-options.js +8 -0
- package/src/{html.js → render/html.js} +7 -23
- package/src/render/index.js +43 -0
- package/src/render/layout.js +37 -0
- package/src/render/render.test.js +111 -0
- package/src/{router.js → router/index.js} +16 -14
- package/src/{router.test.js → router/router.test.js} +9 -9
- package/src/scripts/app.js +9 -3
- package/src/scripts/app.test.js +20 -3
- package/src/store.js +9 -3
- package/src/store.test.js +2 -2
- package/src/build.js +0 -96
- package/src/random.js +0 -30
- package/src/render.js +0 -48
- package/src/render.test.js +0 -72
- package/src/vite-config.js +0 -40
- /package/src/{html.test.js → render/html.test.js} +0 -0
package/README.md
CHANGED
|
@@ -1,882 +1,664 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @inglorious/ssx
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@inglorious/ssx)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
**Static Site Xecution** - Build blazing-fast static sites with [@inglorious/web](https://www.npmjs.com/package/@inglorious/web), complete with server-side rendering, client-side hydration, and zero-config routing.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
```javascript
|
|
11
|
-
// from redux
|
|
12
|
-
import { createStore } from "redux"
|
|
13
|
-
// to
|
|
14
|
-
import { createStore } from "@inglorious/store"
|
|
15
|
-
```
|
|
8
|
+
SSX takes your entity-based web apps and generates optimized static HTML with full hydration support. Think Next.js SSG or Astro, but with the simplicity and predictability of Inglorious Web's entity architecture.
|
|
16
9
|
|
|
17
10
|
---
|
|
18
11
|
|
|
19
|
-
## Why
|
|
12
|
+
## Why SSX?
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
### ⚡️ Fast by Default
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
- **Pre-rendered HTML** - Every page is built at compile time
|
|
17
|
+
- **Instant load times** - No waiting for server responses
|
|
18
|
+
- **CDN-ready** - Deploy anywhere static files are served
|
|
19
|
+
- **Perfect Lighthouse scores** - SEO and performance out of the box
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
### 🎯 Simple Architecture
|
|
26
22
|
|
|
27
|
-
**
|
|
23
|
+
- **No server required** - Pure static files
|
|
24
|
+
- **No complex build configs** - Convention over configuration
|
|
25
|
+
- **File-based routing** - Pages are just files in `src/pages/`
|
|
26
|
+
- **Entity-based state** - Same familiar patterns from @inglorious/web
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
- ✅ Entity-based state (manage multiple instances effortlessly)
|
|
31
|
-
- ✅ No action creators, thunks, or slices
|
|
32
|
-
- ✅ Predictable, testable, purely functional code
|
|
33
|
-
- ✅ Built-in lifecycle events (`add`, `remove`)
|
|
34
|
-
- ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
|
|
28
|
+
### 🔥 Modern DX
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
- **Hot reload dev server** - See changes instantly
|
|
31
|
+
- **Lazy-loaded routes** - Code splitting automatically
|
|
32
|
+
- **lit-html hydration** - Interactive UI without the bloat
|
|
33
|
+
- **TypeScript ready** - Full type support (coming soon)
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
### 🚀 Production Ready
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
- **Automatic code splitting** - Per-page bundles
|
|
38
|
+
- **Optimized builds** - Minified, tree-shaken output
|
|
39
|
+
- **Source maps** - Debug production like development
|
|
40
|
+
- **Error boundaries** - Graceful failure handling
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
// Action creators
|
|
44
|
-
const addTodo = (text) => ({ type: "ADD_TODO", payload: text })
|
|
42
|
+
---
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
const todosReducer = (state = [], action) => {
|
|
48
|
-
switch (action.type) {
|
|
49
|
-
case "ADD_TODO":
|
|
50
|
-
return [...state, { id: Date.now(), text: action.payload }]
|
|
44
|
+
## Quick Start
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
// Handle other action
|
|
46
|
+
### Installation
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
48
|
+
```bash
|
|
49
|
+
npm install @inglorious/ssx @inglorious/web
|
|
50
|
+
```
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
const store = configureStore({
|
|
62
|
-
reducer: {
|
|
63
|
-
work: todosReducer,
|
|
64
|
-
personal: todosReducer,
|
|
65
|
-
},
|
|
66
|
-
})
|
|
52
|
+
### Create Your First Site
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
<!-- ```bash
|
|
55
|
+
npx @inglorious/create-app my-site --template ssx
|
|
56
|
+
cd my-site
|
|
57
|
+
npm run dev
|
|
70
58
|
```
|
|
71
59
|
|
|
72
|
-
|
|
60
|
+
Or manually: -->
|
|
73
61
|
|
|
74
62
|
```javascript
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
// src/pages/index.js
|
|
64
|
+
import { html } from "@inglorious/web"
|
|
65
|
+
|
|
66
|
+
export const index = {
|
|
67
|
+
render() {
|
|
68
|
+
return html`
|
|
69
|
+
<div>
|
|
70
|
+
<h1>Welcome to SSX!</h1>
|
|
71
|
+
<p>This page was pre-rendered at build time.</p>
|
|
72
|
+
<nav>
|
|
73
|
+
<a href="/about">About</a>
|
|
74
|
+
</nav>
|
|
75
|
+
</div>
|
|
76
|
+
`
|
|
84
77
|
},
|
|
85
|
-
|
|
86
|
-
builder.addCase(otherAction, (state, action) => {
|
|
87
|
-
// Handle external action
|
|
88
|
-
})
|
|
89
|
-
},
|
|
90
|
-
})
|
|
78
|
+
}
|
|
91
79
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
export const metadata = {
|
|
81
|
+
title: "Home",
|
|
82
|
+
meta: {
|
|
83
|
+
description: "Welcome to our site",
|
|
84
|
+
"og:image": "/og-image.png",
|
|
96
85
|
},
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
store.dispatch(slice.actions.addTodo("Buy groceries"))
|
|
100
|
-
store.dispatch(otherAction())
|
|
86
|
+
}
|
|
101
87
|
```
|
|
102
88
|
|
|
103
|
-
###
|
|
104
|
-
|
|
105
|
-
```javascript
|
|
106
|
-
// Define entity types and their behavior
|
|
107
|
-
const types = {
|
|
108
|
-
todoList: {
|
|
109
|
-
addTodo(entity, text) {
|
|
110
|
-
entity.todos.push({ id: Date.now(), text })
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
otherAction(entity) {
|
|
114
|
-
// Handle other action
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
}
|
|
89
|
+
### Development
|
|
118
90
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
91
|
+
```bash
|
|
92
|
+
npm run dev
|
|
93
|
+
# → Dev server at http://localhost:3000
|
|
94
|
+
```
|
|
124
95
|
|
|
125
|
-
|
|
126
|
-
const store = createStore({ types, entities })
|
|
96
|
+
### Build
|
|
127
97
|
|
|
128
|
-
|
|
129
|
-
|
|
98
|
+
```bash
|
|
99
|
+
npm run build
|
|
100
|
+
# → Static site in dist/
|
|
101
|
+
```
|
|
130
102
|
|
|
131
|
-
|
|
132
|
-
store.notify("addTodo", "Buy groceries")
|
|
133
|
-
store.notify("otherAction")
|
|
103
|
+
### Deploy
|
|
134
104
|
|
|
135
|
-
|
|
105
|
+
```bash
|
|
106
|
+
npm run preview
|
|
107
|
+
# → Preview production build
|
|
136
108
|
```
|
|
137
109
|
|
|
138
|
-
|
|
110
|
+
Deploy `dist/` to:
|
|
139
111
|
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
112
|
+
- **Vercel** - Zero config
|
|
113
|
+
- **Netlify** - Drop folder
|
|
114
|
+
- **GitHub Pages** - Push and done
|
|
115
|
+
- **Cloudflare Pages** - Instant edge
|
|
116
|
+
- **Any CDN** - It's just files!
|
|
145
117
|
|
|
146
118
|
---
|
|
147
119
|
|
|
148
|
-
##
|
|
120
|
+
## Features
|
|
149
121
|
|
|
150
|
-
###
|
|
122
|
+
### �️ Sitemap & RSS Generation
|
|
151
123
|
|
|
152
|
-
|
|
124
|
+
SSX automatically generates `sitemap.xml` and `rss.xml` based on your pages. Configure them in `src/site.config.js`:
|
|
153
125
|
|
|
154
126
|
```javascript
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (todo) todo.completed = !todo.completed
|
|
163
|
-
},
|
|
127
|
+
export default {
|
|
128
|
+
// Basic metadata
|
|
129
|
+
title: "My Awesome Site",
|
|
130
|
+
meta: {
|
|
131
|
+
description: "A site built with SSX",
|
|
132
|
+
"og:type": "website",
|
|
133
|
+
"og:site_name": "My Site",
|
|
164
134
|
},
|
|
165
135
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
136
|
+
// Sitemap configuration
|
|
137
|
+
sitemap: {
|
|
138
|
+
hostname: "https://myblog.com",
|
|
139
|
+
filter: (page) => !["/admin", "/draft-*", "/test"].includes(page.pattern),
|
|
140
|
+
defaults: {
|
|
141
|
+
changefreq: "weekly",
|
|
142
|
+
priority: 0.5,
|
|
169
143
|
},
|
|
170
144
|
},
|
|
171
|
-
}
|
|
172
145
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
146
|
+
// RSS configuration
|
|
147
|
+
rss: {
|
|
148
|
+
title: "My Blog",
|
|
149
|
+
description: "Latest posts from my blog",
|
|
150
|
+
link: "https://myblog.com",
|
|
151
|
+
feedPath: "/feed.xml",
|
|
152
|
+
language: "en",
|
|
153
|
+
copyright: "© 2026 My Blog",
|
|
154
|
+
maxItems: 10,
|
|
155
|
+
filter: (page) => page.path.startsWith("/posts/"),
|
|
156
|
+
},
|
|
177
157
|
}
|
|
178
158
|
```
|
|
179
159
|
|
|
180
|
-
|
|
160
|
+
Pages with a `published` date in metadata are included in RSS feeds.
|
|
161
|
+
|
|
162
|
+
### �📁 File-Based Routing
|
|
181
163
|
|
|
182
|
-
|
|
183
|
-
- No need to write separate code for each instance
|
|
184
|
-
- Your mental model matches your code structure
|
|
164
|
+
Your file structure defines your routes:
|
|
185
165
|
|
|
186
|
-
|
|
166
|
+
```
|
|
167
|
+
src/pages/
|
|
168
|
+
├── index.js → /
|
|
169
|
+
├── about.js → /about
|
|
170
|
+
├── blog.js → /blog
|
|
171
|
+
└── posts/
|
|
172
|
+
└── _slug.js → /posts/:slug
|
|
173
|
+
```
|
|
187
174
|
|
|
188
|
-
|
|
175
|
+
Dynamic routes use underscore prefix: `_id.js`, `_slug.js`, etc.
|
|
189
176
|
|
|
190
|
-
|
|
177
|
+
### ⚛️ Entity-Based State And Behavior
|
|
191
178
|
|
|
192
179
|
```javascript
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
// src/pages/about.js
|
|
181
|
+
import { html } from "@inglorious/web"
|
|
182
|
+
|
|
183
|
+
export const about = {
|
|
184
|
+
click(entity) {
|
|
185
|
+
entity.name += "!"
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
render(entity, api) {
|
|
189
|
+
return html`<h1>
|
|
190
|
+
About
|
|
191
|
+
<span @click=${() => api.notify(`#${entity.id}:click`)}
|
|
192
|
+
>${entity.name}</span
|
|
193
|
+
>
|
|
194
|
+
</h1>`
|
|
198
195
|
},
|
|
199
196
|
}
|
|
200
197
|
```
|
|
201
198
|
|
|
202
|
-
2. Event handlers accept as arguments the current entity, the event payload, and an API object that exposes a few convenient methods:
|
|
203
|
-
|
|
204
199
|
```javascript
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
api.notify(type, payload) // similar to dispatch. Yes, you can dispatch inside of a reducer!
|
|
211
|
-
api.dispatch(action) // optional, if you prefer Redux-style dispatching
|
|
212
|
-
},
|
|
200
|
+
// src/entities.js
|
|
201
|
+
export const entities = {
|
|
202
|
+
about: {
|
|
203
|
+
type: "about",
|
|
204
|
+
name: "Us",
|
|
213
205
|
},
|
|
214
206
|
}
|
|
215
207
|
```
|
|
216
208
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
## Installation & Setup
|
|
220
|
-
|
|
221
|
-
The Inglorious store, just like Redux, can be used standalone. However, it's commonly used together with component libraries such as React.
|
|
209
|
+
### 🔄 Data Loading
|
|
222
210
|
|
|
223
|
-
|
|
211
|
+
Load data at build time with the `load` export:
|
|
224
212
|
|
|
225
213
|
```javascript
|
|
226
|
-
|
|
227
|
-
import {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
214
|
+
// src/pages/blog.js
|
|
215
|
+
import { html } from "@inglorious/web"
|
|
216
|
+
|
|
217
|
+
export const blog = {
|
|
218
|
+
render(entity) {
|
|
219
|
+
return html`
|
|
220
|
+
<h1>Blog Posts</h1>
|
|
221
|
+
<ul>
|
|
222
|
+
${entity.posts?.map(
|
|
223
|
+
(post) => html`
|
|
224
|
+
<li>
|
|
225
|
+
<a href="/posts/${post.id}">${post.title}</a>
|
|
226
|
+
</li>
|
|
227
|
+
`,
|
|
228
|
+
)}
|
|
229
|
+
</ul>
|
|
230
|
+
`
|
|
238
231
|
},
|
|
239
232
|
}
|
|
240
233
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// 3. Create the store
|
|
247
|
-
const store = createStore({ types, entities })
|
|
248
|
-
|
|
249
|
-
// 4. Provide the store with react-redux
|
|
250
|
-
function App() {
|
|
251
|
-
return (
|
|
252
|
-
<Provider store={store}>
|
|
253
|
-
<Counter />
|
|
254
|
-
</Provider>
|
|
255
|
-
)
|
|
234
|
+
// SSR: Load data during build
|
|
235
|
+
export async function load(entity) {
|
|
236
|
+
const response = await fetch("https://api.example.com/posts")
|
|
237
|
+
entity.posts = await response.json()
|
|
256
238
|
}
|
|
257
239
|
|
|
258
|
-
|
|
259
|
-
function Counter() {
|
|
260
|
-
const dispatch = useDispatch()
|
|
261
|
-
const count = useSelector((state) => state.counter1.value)
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div>
|
|
265
|
-
<p>{count}</p>
|
|
266
|
-
<button onClick={() => dispatch({ type: "increment" })}>+</button>
|
|
267
|
-
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
|
|
268
|
-
</div>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
240
|
+
export const title = "Blog"
|
|
271
241
|
```
|
|
272
242
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
For React applications, `@inglorious/react-store` provides a set of hooks and a Provider that are tightly integrated with the store. It's a lightweight wrapper around `react-redux` that offers a more ergonomic API.
|
|
276
|
-
|
|
277
|
-
```javascript
|
|
278
|
-
import { createStore } from "@inglorious/store"
|
|
279
|
-
import { createReactStore } from "@inglorious/react-store"
|
|
243
|
+
The `load` function runs on the server during build. Data is serialized into the HTML and available immediately on the client.
|
|
280
244
|
|
|
281
|
-
|
|
245
|
+
### 🎨 Dynamic Routes with `staticPaths`
|
|
282
246
|
|
|
283
|
-
|
|
247
|
+
Generate multiple pages from data:
|
|
284
248
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
249
|
+
```javascript
|
|
250
|
+
// src/pages/posts/_slug.js
|
|
251
|
+
import { html } from "@inglorious/web"
|
|
252
|
+
|
|
253
|
+
export const post = {
|
|
254
|
+
render(entity) {
|
|
255
|
+
return html`
|
|
256
|
+
<article>
|
|
257
|
+
<h1>${entity.post.title}</h1>
|
|
258
|
+
<div>${entity.post.body}</div>
|
|
259
|
+
</article>
|
|
260
|
+
`
|
|
261
|
+
},
|
|
292
262
|
}
|
|
293
263
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
return (
|
|
299
|
-
<div>
|
|
300
|
-
<p>{count}</p>
|
|
301
|
-
<button onClick={() => notify("increment")}>+</button> // simplified
|
|
302
|
-
syntax
|
|
303
|
-
<button onClick={() => notify("decrement")}>-</button>
|
|
304
|
-
</div>
|
|
264
|
+
// Load data for a specific post
|
|
265
|
+
export async function load(entity, page) {
|
|
266
|
+
const response = await fetch(
|
|
267
|
+
`https://api.example.com/posts/${page.params.slug}`,
|
|
305
268
|
)
|
|
269
|
+
entity.post = await response.json()
|
|
306
270
|
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
The package is fully typed, providing a great developer experience with TypeScript.
|
|
310
271
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
### 🎮 Entity-Based State
|
|
272
|
+
// Tell SSX which pages to generate
|
|
273
|
+
export async function staticPaths() {
|
|
274
|
+
const response = await fetch(`https://api.example.com/posts`)
|
|
275
|
+
const posts = await response.json()
|
|
316
276
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// The original list of counters:
|
|
323
|
-
const store = configureStore({
|
|
324
|
-
reducer: {
|
|
325
|
-
counter1: counterReducer,
|
|
326
|
-
counter2: counterReducer,
|
|
327
|
-
counter3: counterReducer,
|
|
328
|
-
},
|
|
329
|
-
})
|
|
277
|
+
return posts.map((post) => ({
|
|
278
|
+
params: { slug: post.slug },
|
|
279
|
+
path: `/posts/${post.slug}`,
|
|
280
|
+
}))
|
|
281
|
+
}
|
|
330
282
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
283
|
+
export const metadata = (entity) => ({
|
|
284
|
+
title: entity.post.title ?? "Post",
|
|
285
|
+
meta: {
|
|
286
|
+
description: entity.post.excerpt,
|
|
335
287
|
},
|
|
336
288
|
})
|
|
337
|
-
|
|
338
|
-
// with extra actions to manage adding/removing counters:
|
|
339
|
-
store.dispatch({ type: "addCounter", payload: "counter4" })
|
|
340
289
|
```
|
|
341
290
|
|
|
342
|
-
|
|
291
|
+
### 📄 Page Metadata
|
|
292
|
+
|
|
293
|
+
Export metadata for HTML `<head>`. The `metadata` export can be a plain object or a function:
|
|
343
294
|
|
|
344
295
|
```javascript
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
entity.value++
|
|
349
|
-
},
|
|
296
|
+
export const index = {
|
|
297
|
+
render() {
|
|
298
|
+
return html`<h1>Home</h1>`
|
|
350
299
|
},
|
|
351
300
|
}
|
|
352
301
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
302
|
+
// Static metadata
|
|
303
|
+
export const metadata = {
|
|
304
|
+
title: "My Site",
|
|
305
|
+
meta: {
|
|
306
|
+
description: "An awesome static site",
|
|
307
|
+
"og:image": "/og-image.png",
|
|
308
|
+
},
|
|
357
309
|
}
|
|
358
310
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
- `remove`: removes an entity from the state. Triggers a `destroy` lifecycle event.
|
|
366
|
-
|
|
367
|
-
The lifecycle events can be used to define event handlers similar to constructor and destructor methods in OOP:
|
|
368
|
-
|
|
369
|
-
> Remember: events are broadcast to all entities, just like with reducers! Each handler decides if it should respond. More on that in the section below.
|
|
370
|
-
|
|
371
|
-
```javascript
|
|
372
|
-
const types = {
|
|
373
|
-
counter: {
|
|
374
|
-
create(entity, id) {
|
|
375
|
-
if (entity.id !== id) return // "are you talking to me?"
|
|
376
|
-
entity.createdAt = Date.now()
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
destroy(entity, id) {
|
|
380
|
-
if (entity.id !== id) return // "are you talking to me?"
|
|
381
|
-
entity.destroyedAt = Date.now()
|
|
382
|
-
},
|
|
311
|
+
// Or dynamic metadata (uses entity data)
|
|
312
|
+
export const metadata = (entity) => ({
|
|
313
|
+
title: `${entity.user.name}'s Profile`,
|
|
314
|
+
meta: {
|
|
315
|
+
description: entity.user.bio,
|
|
316
|
+
"og:image": entity.user.avatar,
|
|
383
317
|
},
|
|
384
|
-
}
|
|
318
|
+
})
|
|
385
319
|
```
|
|
386
320
|
|
|
387
|
-
###
|
|
321
|
+
### 🔥 Client-Side Hydration
|
|
388
322
|
|
|
389
|
-
|
|
323
|
+
Pages hydrate automatically with lit-html. Interactivity works immediately:
|
|
390
324
|
|
|
391
325
|
```javascript
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const task = entity.tasks.find((t) => t.id === taskId)
|
|
396
|
-
if (task) task.completed = true
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
stats: {
|
|
400
|
-
taskCompleted(entity, taskId) {
|
|
401
|
-
entity.completedCount++
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
notifications: {
|
|
405
|
-
taskCompleted(entity, taskId) {
|
|
406
|
-
entity.messages.push("Nice! Task completed.")
|
|
407
|
-
},
|
|
326
|
+
export const counter = {
|
|
327
|
+
click(entity) {
|
|
328
|
+
entity.count++
|
|
408
329
|
},
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// One notify call, all three entity types respond
|
|
412
|
-
store.notify("taskCompleted", "task123")
|
|
413
|
-
```
|
|
414
330
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// This runs for EVERY todoList entity, but only acts if it's the right one
|
|
425
|
-
if (entity.id !== id) return
|
|
426
|
-
|
|
427
|
-
const todo = entity.todos.find((t) => t.id === id)
|
|
428
|
-
if (todo) todo.completed = !todo.completed
|
|
429
|
-
},
|
|
331
|
+
render(entity, api) {
|
|
332
|
+
return html`
|
|
333
|
+
<div>
|
|
334
|
+
<p>Count: ${entity.count}</p>
|
|
335
|
+
<button @click=${() => api.notify(`#${entity.id}:click`)}>
|
|
336
|
+
Increment
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
`
|
|
430
340
|
},
|
|
431
341
|
}
|
|
432
|
-
|
|
433
|
-
// Broadcast to all todo lists
|
|
434
|
-
store.notify("toggle", "todo1")
|
|
435
|
-
// Each list's toggle handler runs; only the one with todo1 actually updates
|
|
436
342
|
```
|
|
437
343
|
|
|
438
|
-
|
|
344
|
+
The HTML is pre-rendered on the server. When JavaScript loads, lit-html hydrates the existing DOM and wires up event handlers. No flash of unstyled content, no duplicate rendering.
|
|
439
345
|
|
|
440
|
-
|
|
346
|
+
### 🧭 Client-Side Navigation
|
|
441
347
|
|
|
442
|
-
|
|
348
|
+
After hydration, navigation is instant:
|
|
443
349
|
|
|
444
350
|
```javascript
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
async loadTodos(entity, payload, api) {
|
|
448
|
-
try {
|
|
449
|
-
entity.loading = true
|
|
450
|
-
const { name } = api.getEntity("user")
|
|
451
|
-
const response = await fetch(`/api/todos/${name}`)
|
|
452
|
-
const data = await response.json()
|
|
453
|
-
api.notify("todosLoaded", todos)
|
|
454
|
-
} catch (error) {
|
|
455
|
-
api.notify("loadFailed", error.message)
|
|
456
|
-
}
|
|
457
|
-
},
|
|
351
|
+
// Links navigate without page reload
|
|
352
|
+
;<a href="/about">About</a> // Client-side routing
|
|
458
353
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
entity.loading = false
|
|
462
|
-
},
|
|
354
|
+
// Programmatic navigation
|
|
355
|
+
api.notify("navigate", "/posts")
|
|
463
356
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
357
|
+
// With options
|
|
358
|
+
api.notify("navigate", {
|
|
359
|
+
to: "/posts/123",
|
|
360
|
+
replace: true,
|
|
361
|
+
})
|
|
470
362
|
```
|
|
471
363
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
- **`api.getEntities()`** - read entire state
|
|
475
|
-
- **`api.getEntity(id)`** - read one entity
|
|
476
|
-
- **`api.notify(type, payload)`** - trigger other events (queued, not immediate)
|
|
477
|
-
- **`api.dispatch(action)`** - optional, if you prefer Redux-style dispatching
|
|
478
|
-
- **`api.getTypes()`** - access type definitions (mainly for middleware/plugins)
|
|
479
|
-
- **`api.getType(typeName)`** - access type definition (mainly for overrides)
|
|
364
|
+
Routes are lazy-loaded on demand, keeping initial bundle size small.
|
|
480
365
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
### 🧪 Testing
|
|
484
|
-
|
|
485
|
-
Event handlers are pure functions (or can be treated as such), making them easy to test in isolation, much like Redux reducers. The `@inglorious/store/test` module provides utility functions to make this even simpler.
|
|
366
|
+
---
|
|
486
367
|
|
|
487
|
-
|
|
368
|
+
## CLI
|
|
488
369
|
|
|
489
|
-
|
|
370
|
+
SSX provides a simple CLI for building and developing:
|
|
490
371
|
|
|
491
|
-
|
|
492
|
-
import { trigger } from "@inglorious/store/test"
|
|
372
|
+
### `ssx build`
|
|
493
373
|
|
|
494
|
-
|
|
495
|
-
function increment(entity, payload, api) {
|
|
496
|
-
entity.value += payload.amount
|
|
497
|
-
if (entity.value > 100) {
|
|
498
|
-
api.notify("overflow", { id: entity.id })
|
|
499
|
-
}
|
|
500
|
-
}
|
|
374
|
+
Builds your static site:
|
|
501
375
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{ type: "counter", id: "counter1", value: 99 },
|
|
505
|
-
increment,
|
|
506
|
-
{ amount: 5 },
|
|
507
|
-
)
|
|
376
|
+
```bash
|
|
377
|
+
ssx build [options]
|
|
508
378
|
|
|
509
|
-
|
|
510
|
-
|
|
379
|
+
Options:
|
|
380
|
+
-c, --config <file> Config file (default: "site.config.js")
|
|
381
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
382
|
+
-o, --out <dir> Output directory (default: "dist")
|
|
383
|
+
-i, --incremental Enable incremental builds (default: true)
|
|
384
|
+
-f, --force Force clean build, ignore cache
|
|
511
385
|
```
|
|
512
386
|
|
|
513
|
-
|
|
387
|
+
### `ssx dev`
|
|
514
388
|
|
|
515
|
-
|
|
389
|
+
Starts development server with hot reload:
|
|
516
390
|
|
|
517
|
-
```
|
|
518
|
-
|
|
391
|
+
```bash
|
|
392
|
+
ssx dev [options]
|
|
519
393
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
394
|
+
Options:
|
|
395
|
+
-c, --config <file> Config file (default: "site.config.js")
|
|
396
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
397
|
+
-p, --port <port> Dev server port (default: 3000)
|
|
398
|
+
```
|
|
525
399
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
400
|
+
### Package.json Scripts
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"scripts": {
|
|
405
|
+
"dev": "ssx dev",
|
|
406
|
+
"build": "ssx build",
|
|
407
|
+
"preview": "pnpm dlx serve dist"
|
|
408
|
+
}
|
|
530
409
|
}
|
|
410
|
+
```
|
|
531
411
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
copyValue,
|
|
536
|
-
{ sourceId: "counter1" },
|
|
537
|
-
api,
|
|
538
|
-
)
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Project Structure
|
|
539
415
|
|
|
540
|
-
|
|
416
|
+
```
|
|
417
|
+
my-site/
|
|
418
|
+
├── src/
|
|
419
|
+
│ ├── pages/ # File-based routes
|
|
420
|
+
│ │ ├── index.js # Home page
|
|
421
|
+
│ │ ├── about.js # About page
|
|
422
|
+
│ │ └── posts/
|
|
423
|
+
│ │ ├── index.js # /posts
|
|
424
|
+
│ │ └── _id.js # /posts/:id
|
|
425
|
+
│ ├── entities.js # Entity definitions
|
|
426
|
+
│ └── types/ # Custom entity types (optional)
|
|
427
|
+
├── dist/ # Build output
|
|
428
|
+
├── package.json
|
|
429
|
+
└── site.config.js # Site configuration
|
|
541
430
|
```
|
|
542
431
|
|
|
543
|
-
|
|
432
|
+
---
|
|
544
433
|
|
|
545
|
-
|
|
546
|
-
- `getEntity(id)`: Returns a specific entity by ID (frozen).
|
|
547
|
-
- `dispatch(event)`: Records an event for later assertions.
|
|
548
|
-
- `notify(type, payload)`: A convenience wrapper around `dispatch`.
|
|
549
|
-
- `getEvents()`: Returns all events that were dispatched.
|
|
434
|
+
## Comparison to Other Tools
|
|
550
435
|
|
|
551
|
-
|
|
436
|
+
| Feature | SSX | Next.js (SSG) | Astro | Eleventy |
|
|
437
|
+
| ----------------------- | ----------- | ------------- | ------ | -------- |
|
|
438
|
+
| Pre-rendered HTML | ✅ | ✅ | ✅ | ✅ |
|
|
439
|
+
| Client hydration | ✅ lit-html | ✅ React | ✅ Any | ❌ |
|
|
440
|
+
| Client routing | ✅ | ✅ | ✅ | ❌ |
|
|
441
|
+
| Lazy loading | ✅ | ✅ | ✅ | ❌ |
|
|
442
|
+
| Entity-based state | ✅ | ❌ | ❌ | ❌ |
|
|
443
|
+
| No compilation required | ✅ | ❌ | ❌ | ✅ |
|
|
444
|
+
| Zero config | ✅ | ❌ | ❌ | ❌ |
|
|
445
|
+
| Framework agnostic | ❌ | ❌ | ✅ | ✅ |
|
|
552
446
|
|
|
553
|
-
|
|
447
|
+
SSX is perfect if you:
|
|
554
448
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const allTodos = Object.values(state)
|
|
561
|
-
.filter((e) => e.type === "todoList")
|
|
562
|
-
.flatMap((e) => e.todos)
|
|
563
|
-
|
|
564
|
-
// Update global stats
|
|
565
|
-
state.stats.total = allTodos.length
|
|
566
|
-
state.stats.completed = allTodos.filter((t) => t.completed).length
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
]
|
|
449
|
+
- Want static site performance
|
|
450
|
+
- Love entity-based architecture
|
|
451
|
+
- Prefer convention over configuration
|
|
452
|
+
- Need full client-side interactivity
|
|
453
|
+
- Don't want React/Vue lock-in
|
|
570
454
|
|
|
571
|
-
|
|
572
|
-
```
|
|
455
|
+
---
|
|
573
456
|
|
|
574
|
-
|
|
457
|
+
## Advanced Usage
|
|
575
458
|
|
|
576
|
-
###
|
|
459
|
+
### Site Configuration
|
|
577
460
|
|
|
578
|
-
|
|
461
|
+
Customize SSX behavior in `src/site.config.js`:
|
|
579
462
|
|
|
580
463
|
```javascript
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
464
|
+
export default {
|
|
465
|
+
// Basic metadata
|
|
466
|
+
lang: "en",
|
|
467
|
+
charset: "UTF-8",
|
|
468
|
+
title: "My Awesome Site",
|
|
469
|
+
meta: {
|
|
470
|
+
description: "A site built with SSX",
|
|
471
|
+
"og:type": "website",
|
|
585
472
|
},
|
|
586
473
|
|
|
587
|
-
|
|
588
|
-
|
|
474
|
+
// Global assets
|
|
475
|
+
styles: ["./styles/reset.css", "./styles/theme.css"],
|
|
476
|
+
scripts: ["./scripts/analytics.js"],
|
|
477
|
+
|
|
478
|
+
// Build options
|
|
479
|
+
basePath: "/",
|
|
480
|
+
rootDir: "src",
|
|
481
|
+
outDir: "dist",
|
|
482
|
+
publicDir: "public",
|
|
483
|
+
favicon: "/favicon.ico",
|
|
484
|
+
|
|
485
|
+
// Router config
|
|
486
|
+
router: {
|
|
487
|
+
trailingSlash: false,
|
|
488
|
+
scrollBehavior: "smooth",
|
|
589
489
|
},
|
|
590
|
-
}
|
|
591
490
|
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
entity.value = 0
|
|
491
|
+
// Vite config passthrough
|
|
492
|
+
vite: {
|
|
493
|
+
server: {
|
|
494
|
+
port: 3000,
|
|
495
|
+
open: true,
|
|
598
496
|
},
|
|
599
497
|
},
|
|
600
|
-
]
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
A behavior is defined as either an object with event handlers, or a function that takes a type and returns an enhanced behavior (decorator pattern):
|
|
604
498
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
entity.value = ""
|
|
499
|
+
// Build hooks
|
|
500
|
+
hooks: {
|
|
501
|
+
beforeBuild: async (config) => console.log("Starting build..."),
|
|
502
|
+
afterBuild: async (result) => console.log(`Built ${result.pages} pages`),
|
|
610
503
|
},
|
|
611
504
|
}
|
|
612
|
-
|
|
613
|
-
// Function that wraps and enhances a behavior
|
|
614
|
-
const validated = (type) => ({
|
|
615
|
-
submit(entity, value, api) {
|
|
616
|
-
if (!value.trim()) return
|
|
617
|
-
type.submit?.(entity, value, api) // remember to always pass all args!
|
|
618
|
-
},
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
// Another wrapper
|
|
622
|
-
const withLoading = (type) => ({
|
|
623
|
-
submit(entity, value, api) {
|
|
624
|
-
entity.loading = true
|
|
625
|
-
type.submit?.(entity, value, api)
|
|
626
|
-
entity.loading = false
|
|
627
|
-
},
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
// Compose them together to form a type
|
|
631
|
-
const form = [resettable, validated, withLoading]
|
|
632
505
|
```
|
|
633
506
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
### ⏱️ Batched Mode
|
|
507
|
+
### Environment Variables
|
|
637
508
|
|
|
638
|
-
|
|
509
|
+
Use Vite's environment variables:
|
|
639
510
|
|
|
640
511
|
```javascript
|
|
641
|
-
|
|
512
|
+
// Access in your code
|
|
513
|
+
const apiUrl = import.meta.env.VITE_API_URL
|
|
642
514
|
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
store.notify("enemyAttacked", { damage: 10 })
|
|
646
|
-
store.notify("particleCreated", { type: "explosion" })
|
|
647
|
-
|
|
648
|
-
// process them all in batch
|
|
649
|
-
store.update()
|
|
515
|
+
// .env file
|
|
516
|
+
VITE_API_URL=https://api.example.com
|
|
650
517
|
```
|
|
651
518
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## Comparison with Other State Libraries
|
|
657
|
-
|
|
658
|
-
| Feature | Redux | RTK | Zustand | Jotai | Pinia | MobX | Inglorious Store |
|
|
659
|
-
| ------------------------- | ------------ | ------------ | ---------- | ---------- | ---------- | ---------- | ---------------- |
|
|
660
|
-
| **Boilerplate** | 🔴 High | 🟡 Medium | 🟢 Low | 🟢 Low | 🟡 Medium | 🟢 Low | 🟢 Low |
|
|
661
|
-
| **Multiple instances** | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟡 Medium | 🟡 Medium | 🟢 Built-in |
|
|
662
|
-
| **Lifecycle events** | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🔴 No | 🟢 Yes |
|
|
663
|
-
| **Async logic placement** | 🟡 Thunks | 🟡 Complex | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 Free | 🟢 In handlers |
|
|
664
|
-
| **Redux DevTools** | 🟢 Yes | 🟢 Yes | 🟡 Partial | 🟡 Partial | 🟡 Partial | 🟢 Yes | 🟢 Yes |
|
|
665
|
-
| **Time-travel debugging** | 🟢 Yes | 🟢 Yes | 🔴 No | 🔴 No | 🔴 No | 🟡 Limited | 🟢 Yes |
|
|
666
|
-
| **Testability** | 🟢 Excellent | 🟢 Excellent | 🟡 Good | 🟡 Good | 🟡 Good | 🟡 Medium | 🟢 Excellent |
|
|
667
|
-
| **Immutability** | 🔴 Manual | 🟢 Immer | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🔴 Manual | 🟢 Mutative |
|
|
668
|
-
|
|
669
|
-
---
|
|
670
|
-
|
|
671
|
-
## API Reference
|
|
519
|
+
### Custom 404 Page
|
|
672
520
|
|
|
673
|
-
|
|
521
|
+
Create a fallback route:
|
|
674
522
|
|
|
675
523
|
```javascript
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
524
|
+
// src/pages/404.js
|
|
525
|
+
export const notFound = {
|
|
526
|
+
render() {
|
|
527
|
+
return html`
|
|
528
|
+
<div>
|
|
529
|
+
<h1>404 - Page Not Found</h1>
|
|
530
|
+
<a href="/">Go Home</a>
|
|
531
|
+
</div>
|
|
532
|
+
`
|
|
533
|
+
},
|
|
534
|
+
}
|
|
687
535
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
entityType: [
|
|
691
|
-
// Behavior objects
|
|
692
|
-
{
|
|
693
|
-
eventName(entity, payload, api) {
|
|
694
|
-
entity.value = payload
|
|
695
|
-
api.notify("otherEvent", data)
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
// Behavior functions (decorators)
|
|
699
|
-
(behavior) => ({
|
|
700
|
-
eventName(entity, payload, api) {
|
|
701
|
-
// Wrap the behavior
|
|
702
|
-
behavior.eventName?.(entity, payload, api)
|
|
703
|
-
},
|
|
704
|
-
}),
|
|
705
|
-
],
|
|
536
|
+
export const metadata = {
|
|
537
|
+
title: "404",
|
|
706
538
|
}
|
|
707
539
|
```
|
|
708
540
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
Each handler receives three arguments:
|
|
712
|
-
|
|
713
|
-
- **`entity`** - the entity instance (mutate freely, immutability guaranteed)
|
|
714
|
-
- **`payload`** - data passed with the event
|
|
715
|
-
- **`api`** - access to store methods:
|
|
716
|
-
- `getEntities()` - entire state (read-only)
|
|
717
|
-
- `getEntity(id)` - single entity (read-only)
|
|
718
|
-
- `notify(type, payload)` - trigger other events
|
|
719
|
-
- `dispatch(action)` - optional, if you prefer Redux-style dispatching
|
|
720
|
-
- `getTypes()` - type definitions (for middleware)
|
|
721
|
-
- `getType(typeName)` - type definition (for overriding)
|
|
722
|
-
- `setType(typeName, type)` - change the behavior of a type
|
|
723
|
-
|
|
724
|
-
### Built-in Events
|
|
725
|
-
|
|
726
|
-
- **`create(entity)`** - triggered when entity added via `add` event, visible only to that entity
|
|
727
|
-
- **`destroy(entity)`** - triggered when entity removed via `remove` event, visible only to that entity
|
|
728
|
-
|
|
729
|
-
### Notify vs Dispatch
|
|
730
|
-
|
|
731
|
-
Both work (`dispatch` is provided just for Redux compatibility), but `notify` is cleaner (and uses `dispatch` internally):
|
|
541
|
+
Register it in your router:
|
|
732
542
|
|
|
733
543
|
```javascript
|
|
734
|
-
|
|
735
|
-
|
|
544
|
+
// src/entities.js
|
|
545
|
+
import { setRoutes } from "@inglorious/web/router"
|
|
546
|
+
|
|
547
|
+
setRoutes({
|
|
548
|
+
// ... other routes
|
|
549
|
+
"*": "notFound", // Fallback
|
|
550
|
+
})
|
|
736
551
|
```
|
|
737
552
|
|
|
738
|
-
###
|
|
553
|
+
### Incremental Builds
|
|
739
554
|
|
|
740
|
-
|
|
555
|
+
SSX enables incremental builds by default. Only changed pages are rebuilt, dramatically speeding up your build process:
|
|
741
556
|
|
|
742
|
-
|
|
557
|
+
```bash
|
|
558
|
+
ssx build
|
|
559
|
+
# Only changed pages are rebuilt
|
|
743
560
|
|
|
744
|
-
|
|
561
|
+
ssx build --force
|
|
562
|
+
# Force a clean rebuild of all pages
|
|
563
|
+
```
|
|
745
564
|
|
|
746
|
-
|
|
565
|
+
Incremental builds respect your page dependencies and invalidate cache when dependencies change.
|
|
747
566
|
|
|
748
|
-
|
|
567
|
+
---
|
|
749
568
|
|
|
750
|
-
|
|
751
|
-
// src/store/types.ts
|
|
752
|
-
import type {
|
|
753
|
-
FormEntity,
|
|
754
|
-
ListEntity,
|
|
755
|
-
FooterEntity,
|
|
756
|
-
// ... other payload types
|
|
757
|
-
} from "../../types"
|
|
569
|
+
## API Reference
|
|
758
570
|
|
|
759
|
-
|
|
760
|
-
interface TodoListTypes {
|
|
761
|
-
form: {
|
|
762
|
-
inputChange: (entity: FormEntity, value: string) => void
|
|
763
|
-
formSubmit: (entity: FormEntity) => void
|
|
764
|
-
}
|
|
765
|
-
list: {
|
|
766
|
-
formSubmit: (entity: ListEntity, value: string) => void
|
|
767
|
-
toggleClick: (entity: ListEntity, id: number) => void
|
|
768
|
-
// ... other handlers
|
|
769
|
-
}
|
|
770
|
-
footer: {
|
|
771
|
-
filterClick: (entity: FooterEntity, id: string) => void
|
|
772
|
-
}
|
|
773
|
-
}
|
|
571
|
+
### Build API
|
|
774
572
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
573
|
+
```javascript
|
|
574
|
+
import { build } from "@inglorious/ssx/build"
|
|
575
|
+
|
|
576
|
+
await build({
|
|
577
|
+
rootDir: "src",
|
|
578
|
+
outDir: "dist",
|
|
579
|
+
configFile: "site.config.js",
|
|
580
|
+
incremental: true,
|
|
581
|
+
clean: false,
|
|
582
|
+
})
|
|
786
583
|
```
|
|
787
584
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
**2. Create the Store**
|
|
585
|
+
### Dev Server API
|
|
791
586
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
// src/store/index.ts
|
|
796
|
-
import { createStore, type TypesConfig } from "@inglorious/store"
|
|
797
|
-
import { types } from "./types"
|
|
798
|
-
import type { TodoListEntity, TodoListState } from "../../types"
|
|
587
|
+
```javascript
|
|
588
|
+
import { dev } from "@inglorious/ssx/dev"
|
|
799
589
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
590
|
+
await dev({
|
|
591
|
+
rootDir: "src",
|
|
592
|
+
port: 3000,
|
|
593
|
+
configFile: "site.config.js",
|
|
803
594
|
})
|
|
804
595
|
```
|
|
805
596
|
|
|
806
|
-
**3. Enjoy Full Type Safety**
|
|
807
|
-
|
|
808
|
-
Now, your store is fully type-safe. The hooks provided by `@inglorious/react-store` will also be correctly typed.
|
|
809
|
-
|
|
810
597
|
---
|
|
811
598
|
|
|
812
|
-
##
|
|
599
|
+
<!-- ## Examples
|
|
813
600
|
|
|
814
|
-
|
|
601
|
+
Check out these example projects:
|
|
815
602
|
|
|
816
|
-
-
|
|
817
|
-
-
|
|
818
|
-
-
|
|
819
|
-
-
|
|
820
|
-
- 🔄 Undo/redo, time-travel debugging
|
|
603
|
+
- **[Basic Blog](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-blog)** - Simple blog with posts
|
|
604
|
+
- **[Documentation Site](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-docs)** - Multi-page docs
|
|
605
|
+
- **[E-commerce](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-shop)** - Product catalog
|
|
606
|
+
- **[Portfolio](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-portfolio)** - Personal portfolio
|
|
821
607
|
|
|
822
|
-
|
|
608
|
+
--- -->
|
|
823
609
|
|
|
824
|
-
|
|
825
|
-
- Migration path from Redux (keep using react-redux)
|
|
610
|
+
## Roadmap
|
|
826
611
|
|
|
827
|
-
|
|
612
|
+
- [ ] TypeScript support
|
|
613
|
+
- [ ] Image optimization
|
|
614
|
+
- [ ] API routes (serverless functions)
|
|
615
|
+
- [ ] MDX support
|
|
616
|
+
- [ ] i18n helpers
|
|
828
617
|
|
|
829
|
-
|
|
618
|
+
---
|
|
830
619
|
|
|
831
|
-
|
|
620
|
+
## Philosophy
|
|
832
621
|
|
|
833
|
-
|
|
622
|
+
SSX embraces the philosophy of [@inglorious/web](https://www.npmjs.com/package/@inglorious/web):
|
|
834
623
|
|
|
835
|
-
- **
|
|
836
|
-
- **
|
|
837
|
-
- **
|
|
838
|
-
- **
|
|
624
|
+
- **Simplicity over cleverness** - Obvious beats clever
|
|
625
|
+
- **Convention over configuration** - Sensible defaults
|
|
626
|
+
- **Predictability over magic** - Explicit is better than implicit
|
|
627
|
+
- **Standards over abstractions** - Use the platform
|
|
839
628
|
|
|
840
|
-
|
|
629
|
+
Static site generation should be simple. SSX makes it simple.
|
|
841
630
|
|
|
842
631
|
---
|
|
843
632
|
|
|
844
|
-
##
|
|
845
|
-
|
|
846
|
-
It's hard to accept the new, especially on Reddit. Here are the main objections to the Inglorious Store.
|
|
633
|
+
## Contributing
|
|
847
634
|
|
|
848
|
-
|
|
635
|
+
Contributions are welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
|
849
636
|
|
|
850
|
-
|
|
637
|
+
---
|
|
851
638
|
|
|
852
|
-
|
|
639
|
+
## License
|
|
853
640
|
|
|
854
|
-
|
|
855
|
-
| ------------------------------------- | -------------------------------------- |
|
|
856
|
-
| Entities are ids | Entities have an id |
|
|
857
|
-
| Components are pure, consecutive data | Entities are pure bags of related data |
|
|
858
|
-
| Data and behavior are separated | Data and behavior are separated |
|
|
859
|
-
| Systems operate on the whole state | Systems operate on the whole state |
|
|
860
|
-
| Usually written in an OOP environment | Written in an FP environment |
|
|
641
|
+
**MIT License** - Free and open source
|
|
861
642
|
|
|
862
|
-
|
|
643
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
863
644
|
|
|
864
|
-
|
|
645
|
+
---
|
|
865
646
|
|
|
866
|
-
|
|
647
|
+
## Related Packages
|
|
867
648
|
|
|
868
|
-
|
|
649
|
+
- [@inglorious/web](https://www.npmjs.com/package/@inglorious/web) - Entity-based web framework
|
|
650
|
+
- [@inglorious/store](https://www.npmjs.com/package/@inglorious/store) - State management
|
|
651
|
+
- [@inglorious/engine](https://www.npmjs.com/package/@inglorious/engine) - Game engine
|
|
869
652
|
|
|
870
653
|
---
|
|
871
654
|
|
|
872
|
-
##
|
|
873
|
-
|
|
874
|
-
MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
655
|
+
## Support
|
|
875
656
|
|
|
876
|
-
|
|
657
|
+
- 📖 [Documentation](https://inglorious-engine.vercel.app)
|
|
658
|
+
- 💬 [Discord Community](https://discord.gg/Byx85t2eFp)
|
|
659
|
+
- 🐛 [Issue Tracker](https://github.com/IngloriousCoderz/inglorious-forge/issues)
|
|
660
|
+
- 📧 [Email Support](mailto:antony.mistretta@gmail.com)
|
|
877
661
|
|
|
878
662
|
---
|
|
879
663
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
|
664
|
+
**Build static sites the Inglorious way. Simple. Predictable. Fast.** 🚀
|