@inglorious/ssx 0.4.1 → 1.0.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 +380 -677
- package/package.json +1 -1
- package/src/render.test.js +4 -4
- package/src/router.test.js +5 -5
- package/src/scripts/app.test.js +3 -3
- package/src/store.js +9 -3
- package/src/store.test.js +2 -2
package/README.md
CHANGED
|
@@ -1,882 +1,585 @@
|
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
### Redux
|
|
41
|
-
|
|
42
|
-
```javascript
|
|
43
|
-
// Action creators
|
|
44
|
-
const addTodo = (text) => ({ type: "ADD_TODO", payload: text })
|
|
35
|
+
### 🚀 Production Ready
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return [...state, { id: Date.now(), text: action.payload }]
|
|
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
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
// Handle other action
|
|
42
|
+
---
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
return state
|
|
57
|
-
}
|
|
58
|
-
}
|
|
44
|
+
## Quick Start
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
const store = configureStore({
|
|
62
|
-
reducer: {
|
|
63
|
-
work: todosReducer,
|
|
64
|
-
personal: todosReducer,
|
|
65
|
-
},
|
|
66
|
-
})
|
|
46
|
+
### Installation
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
```bash
|
|
49
|
+
npm install @inglorious/ssx @inglorious/web
|
|
70
50
|
```
|
|
71
51
|
|
|
72
|
-
###
|
|
52
|
+
### Create Your First Site
|
|
73
53
|
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
name: "todos",
|
|
79
|
-
initialState: [],
|
|
80
|
-
reducers: {
|
|
81
|
-
addTodo: (state, action) => {
|
|
82
|
-
state.push({ id: Date.now(), text: action.payload })
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
extraReducers: (builder) => {
|
|
86
|
-
builder.addCase(otherAction, (state, action) => {
|
|
87
|
-
// Handle external action
|
|
88
|
-
})
|
|
89
|
-
},
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const store = configureStore({
|
|
93
|
-
reducer: {
|
|
94
|
-
work: todosSlice.reducer,
|
|
95
|
-
personal: todosSlice.reducer,
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
store.dispatch(slice.actions.addTodo("Buy groceries"))
|
|
100
|
-
store.dispatch(otherAction())
|
|
54
|
+
<!-- ```bash
|
|
55
|
+
npx @inglorious/create-app my-site --template ssx
|
|
56
|
+
cd my-site
|
|
57
|
+
npm run dev
|
|
101
58
|
```
|
|
102
59
|
|
|
103
|
-
|
|
60
|
+
Or manually: -->
|
|
104
61
|
|
|
105
62
|
```javascript
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
`
|
|
116
77
|
},
|
|
117
78
|
}
|
|
118
79
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
work: { type: "todoList", todos: [] },
|
|
122
|
-
personal: { type: "todoList", todos: [] },
|
|
123
|
-
}
|
|
80
|
+
export const title = "Home"
|
|
81
|
+
```
|
|
124
82
|
|
|
125
|
-
|
|
126
|
-
const store = createStore({ types, entities })
|
|
83
|
+
### Development
|
|
127
84
|
|
|
128
|
-
|
|
129
|
-
|
|
85
|
+
```bash
|
|
86
|
+
npm run dev
|
|
87
|
+
# → Dev server at http://localhost:3000
|
|
88
|
+
```
|
|
130
89
|
|
|
131
|
-
|
|
132
|
-
store.notify("addTodo", "Buy groceries")
|
|
133
|
-
store.notify("otherAction")
|
|
90
|
+
### Build
|
|
134
91
|
|
|
135
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
# → Static site in dist/
|
|
136
95
|
```
|
|
137
96
|
|
|
138
|
-
|
|
97
|
+
### Deploy
|
|
139
98
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- ✅ Add multiple instances by adding entities, not code
|
|
99
|
+
```bash
|
|
100
|
+
npm run preview
|
|
101
|
+
# → Preview production build
|
|
102
|
+
```
|
|
145
103
|
|
|
146
|
-
|
|
104
|
+
Deploy `dist/` to:
|
|
147
105
|
|
|
148
|
-
|
|
106
|
+
- **Vercel** - Zero config
|
|
107
|
+
- **Netlify** - Drop folder
|
|
108
|
+
- **GitHub Pages** - Push and done
|
|
109
|
+
- **Cloudflare Pages** - Instant edge
|
|
110
|
+
- **Any CDN** - It's just files!
|
|
149
111
|
|
|
150
|
-
|
|
112
|
+
---
|
|
151
113
|
|
|
152
|
-
|
|
114
|
+
## Features
|
|
153
115
|
|
|
154
|
-
|
|
155
|
-
const types = {
|
|
156
|
-
todoList: {
|
|
157
|
-
addTodo(entity, text) {
|
|
158
|
-
entity.todos.push({ id: Date.now(), text })
|
|
159
|
-
},
|
|
160
|
-
toggle(entity, id) {
|
|
161
|
-
const todo = entity.todos.find((t) => t.id === id)
|
|
162
|
-
if (todo) todo.completed = !todo.completed
|
|
163
|
-
},
|
|
164
|
-
},
|
|
116
|
+
### 📁 File-Based Routing
|
|
165
117
|
|
|
166
|
-
|
|
167
|
-
setTheme(entity, theme) {
|
|
168
|
-
entity.theme = theme
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
}
|
|
118
|
+
Your file structure defines your routes:
|
|
172
119
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
120
|
+
```
|
|
121
|
+
src/pages/
|
|
122
|
+
├── index.js → /
|
|
123
|
+
├── about.js → /about
|
|
124
|
+
├── blog.js → /blog
|
|
125
|
+
└── posts/
|
|
126
|
+
└── _slug.js → /posts/:slug
|
|
178
127
|
```
|
|
179
128
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- Same behavior applies to all instances of that type
|
|
183
|
-
- No need to write separate code for each instance
|
|
184
|
-
- Your mental model matches your code structure
|
|
129
|
+
Dynamic routes use underscore prefix: `_id.js`, `_slug.js`, etc.
|
|
185
130
|
|
|
186
|
-
###
|
|
131
|
+
### ⚛️ Entity-Based State And Behavior
|
|
187
132
|
|
|
188
|
-
|
|
133
|
+
```javascript
|
|
134
|
+
// src/pages/about.js
|
|
135
|
+
import { html } from "@inglorious/web"
|
|
189
136
|
|
|
190
|
-
|
|
137
|
+
export const about = {
|
|
138
|
+
click(entity) {
|
|
139
|
+
entity.name += "!"
|
|
140
|
+
},
|
|
191
141
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
142
|
+
render(entity, api) {
|
|
143
|
+
return html`<h1>
|
|
144
|
+
About
|
|
145
|
+
<span @click=${() => api.notify(`#${entity.id}:click`)}
|
|
146
|
+
>${entity.name}</span
|
|
147
|
+
>
|
|
148
|
+
</h1>`
|
|
198
149
|
},
|
|
199
150
|
}
|
|
200
151
|
```
|
|
201
152
|
|
|
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
153
|
```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
|
-
},
|
|
154
|
+
// src/entities.js
|
|
155
|
+
export const entities = {
|
|
156
|
+
about: {
|
|
157
|
+
type: "about",
|
|
158
|
+
name: "Us",
|
|
213
159
|
},
|
|
214
160
|
}
|
|
215
161
|
```
|
|
216
162
|
|
|
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.
|
|
163
|
+
### 🔄 Data Loading
|
|
222
164
|
|
|
223
|
-
|
|
165
|
+
Load data at build time with the `load` export:
|
|
224
166
|
|
|
225
167
|
```javascript
|
|
226
|
-
|
|
227
|
-
import {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
168
|
+
// src/pages/blog.js
|
|
169
|
+
import { html } from "@inglorious/web"
|
|
170
|
+
|
|
171
|
+
export const blog = {
|
|
172
|
+
render(entity) {
|
|
173
|
+
return html`
|
|
174
|
+
<h1>Blog Posts</h1>
|
|
175
|
+
<ul>
|
|
176
|
+
${entity.posts?.map(
|
|
177
|
+
(post) => html`
|
|
178
|
+
<li>
|
|
179
|
+
<a href="/posts/${post.id}">${post.title}</a>
|
|
180
|
+
</li>
|
|
181
|
+
`,
|
|
182
|
+
)}
|
|
183
|
+
</ul>
|
|
184
|
+
`
|
|
238
185
|
},
|
|
239
186
|
}
|
|
240
187
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
188
|
+
// SSR: Load data during build
|
|
189
|
+
export async function load(entity) {
|
|
190
|
+
const response = await fetch("https://api.example.com/posts")
|
|
191
|
+
entity.posts = await response.json()
|
|
244
192
|
}
|
|
245
193
|
|
|
246
|
-
|
|
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
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// 5. Wire components to the store
|
|
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
|
-
}
|
|
194
|
+
export const title = "Blog"
|
|
271
195
|
```
|
|
272
196
|
|
|
273
|
-
|
|
197
|
+
The `load` function runs on the server during build. Data is serialized into the HTML and available immediately on the client.
|
|
274
198
|
|
|
275
|
-
|
|
199
|
+
### 🎨 Dynamic Routes with `getStaticPaths`
|
|
276
200
|
|
|
277
|
-
|
|
278
|
-
import { createStore } from "@inglorious/store"
|
|
279
|
-
import { createReactStore } from "@inglorious/react-store"
|
|
280
|
-
|
|
281
|
-
const store = createStore({ types, entities })
|
|
282
|
-
|
|
283
|
-
export const { Provider, useSelector, useNotify } = createReactStore(store)
|
|
201
|
+
Generate multiple pages from data:
|
|
284
202
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
203
|
+
```javascript
|
|
204
|
+
// src/pages/posts/_slug.js
|
|
205
|
+
import { html } from "@inglorious/web"
|
|
206
|
+
|
|
207
|
+
export const post = {
|
|
208
|
+
render(entity) {
|
|
209
|
+
return html`
|
|
210
|
+
<article>
|
|
211
|
+
<h1>${entity.post.title}</h1>
|
|
212
|
+
<div>${entity.post.body}</div>
|
|
213
|
+
</article>
|
|
214
|
+
`
|
|
215
|
+
},
|
|
292
216
|
}
|
|
293
217
|
|
|
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>
|
|
218
|
+
// Load data for a specific post
|
|
219
|
+
export async function load(entity, page) {
|
|
220
|
+
const response = await fetch(
|
|
221
|
+
`https://api.example.com/posts/${page.params.slug}`,
|
|
305
222
|
)
|
|
223
|
+
entity.post = await response.json()
|
|
306
224
|
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
The package is fully typed, providing a great developer experience with TypeScript.
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## Core Features
|
|
314
|
-
|
|
315
|
-
### 🎮 Entity-Based State
|
|
316
225
|
|
|
317
|
-
|
|
226
|
+
// Tell SSX which pages to generate
|
|
227
|
+
export async function getStaticPaths() {
|
|
228
|
+
const response = await fetch(`https://api.example.com/posts`)
|
|
229
|
+
const posts = await response.json()
|
|
318
230
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
reducer: {
|
|
325
|
-
counter1: counterReducer,
|
|
326
|
-
counter2: counterReducer,
|
|
327
|
-
counter3: counterReducer,
|
|
328
|
-
},
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
// becomes:
|
|
332
|
-
const store = configureStore({
|
|
333
|
-
reducer: {
|
|
334
|
-
counters: countersReducer,
|
|
335
|
-
},
|
|
336
|
-
})
|
|
231
|
+
return posts.map((post) => ({
|
|
232
|
+
params: { id: post.id },
|
|
233
|
+
path: `/posts/${post.id}`,
|
|
234
|
+
}))
|
|
235
|
+
}
|
|
337
236
|
|
|
338
|
-
|
|
339
|
-
store.dispatch({ type: "addCounter", payload: "counter4" })
|
|
237
|
+
export const title = (entity) => entity.post.title ?? "Post"
|
|
340
238
|
```
|
|
341
239
|
|
|
342
|
-
|
|
240
|
+
### 📄 Page Metadata
|
|
241
|
+
|
|
242
|
+
Export metadata for HTML `<head>`:
|
|
343
243
|
|
|
344
244
|
```javascript
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
entity.value++
|
|
349
|
-
},
|
|
245
|
+
export const index = {
|
|
246
|
+
render() {
|
|
247
|
+
return html`<h1>Home</h1>`
|
|
350
248
|
},
|
|
351
249
|
}
|
|
352
250
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
251
|
+
// Static metadata
|
|
252
|
+
export const title = "My Site"
|
|
253
|
+
export const meta = {
|
|
254
|
+
description: "An awesome static site",
|
|
255
|
+
"og:image": "/og-image.png",
|
|
357
256
|
}
|
|
358
257
|
|
|
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
|
-
},
|
|
258
|
+
// Or dynamic metadata (uses entity data)
|
|
259
|
+
export const title = (entity) => `${entity.user.name}'s Profile`
|
|
260
|
+
export const meta = (entity) => ({
|
|
261
|
+
description: entity.user.bio,
|
|
262
|
+
"og:image": entity.user.avatar,
|
|
263
|
+
})
|
|
378
264
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
}
|
|
265
|
+
// Include CSS/JS files
|
|
266
|
+
export const styles = ["./home.css", "./theme.css"]
|
|
267
|
+
export const scripts = ["./analytics.js"]
|
|
385
268
|
```
|
|
386
269
|
|
|
387
|
-
###
|
|
270
|
+
### 🔥 Client-Side Hydration
|
|
388
271
|
|
|
389
|
-
|
|
272
|
+
Pages hydrate automatically with lit-html. Interactivity works immediately:
|
|
390
273
|
|
|
391
274
|
```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
|
-
},
|
|
275
|
+
export const counter = {
|
|
276
|
+
click(entity) {
|
|
277
|
+
entity.count++
|
|
403
278
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
279
|
+
|
|
280
|
+
render(entity, api) {
|
|
281
|
+
return html`
|
|
282
|
+
<div>
|
|
283
|
+
<p>Count: ${entity.count}</p>
|
|
284
|
+
<button @click=${() => api.notify(`#${entity.id}:click`)}>
|
|
285
|
+
Increment
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
`
|
|
408
289
|
},
|
|
409
290
|
}
|
|
410
|
-
|
|
411
|
-
// One notify call, all three entity types respond
|
|
412
|
-
store.notify("taskCompleted", "task123")
|
|
413
291
|
```
|
|
414
292
|
|
|
415
|
-
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
### 🧭 Client-Side Navigation
|
|
416
296
|
|
|
417
|
-
|
|
418
|
-
- What if you want to notify the event only on one entity of that type? Add an if that checks if the entity should be bothered or not by it.
|
|
297
|
+
After hydration, navigation is instant:
|
|
419
298
|
|
|
420
299
|
```javascript
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
toggle(entity, id) {
|
|
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
|
-
},
|
|
430
|
-
},
|
|
431
|
-
}
|
|
300
|
+
// Links navigate without page reload
|
|
301
|
+
;<a href="/about">About</a> // Client-side routing
|
|
432
302
|
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
303
|
+
// Programmatic navigation
|
|
304
|
+
api.notify("navigate", "/posts")
|
|
305
|
+
|
|
306
|
+
// With options
|
|
307
|
+
api.notify("navigate", {
|
|
308
|
+
to: "/posts/123",
|
|
309
|
+
replace: true,
|
|
310
|
+
})
|
|
436
311
|
```
|
|
437
312
|
|
|
438
|
-
|
|
313
|
+
Routes are lazy-loaded on demand, keeping initial bundle size small.
|
|
439
314
|
|
|
440
|
-
|
|
315
|
+
---
|
|
441
316
|
|
|
442
|
-
|
|
317
|
+
## CLI
|
|
443
318
|
|
|
444
|
-
|
|
445
|
-
const types = {
|
|
446
|
-
todoList: {
|
|
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
|
-
},
|
|
319
|
+
SSX provides a simple CLI for building and developing:
|
|
458
320
|
|
|
459
|
-
|
|
460
|
-
entity.todos = todos
|
|
461
|
-
entity.loading = false
|
|
462
|
-
},
|
|
463
|
-
|
|
464
|
-
loadFailed(entity, error) {
|
|
465
|
-
entity.error = error
|
|
466
|
-
entity.loading = false
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
}
|
|
470
|
-
```
|
|
321
|
+
### `ssx build`
|
|
471
322
|
|
|
472
|
-
|
|
323
|
+
Builds your static site:
|
|
473
324
|
|
|
474
|
-
|
|
475
|
-
|
|
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)
|
|
325
|
+
```bash
|
|
326
|
+
ssx build [options]
|
|
480
327
|
|
|
481
|
-
|
|
328
|
+
Options:
|
|
329
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
330
|
+
-o, --out <dir> Output directory (default: "dist")
|
|
331
|
+
-t, --title <title> Default page title (default: "My Site")
|
|
332
|
+
--styles <styles...> Global CSS files
|
|
333
|
+
--scripts <scripts...> Global JS files
|
|
334
|
+
```
|
|
482
335
|
|
|
483
|
-
###
|
|
336
|
+
### `ssx dev`
|
|
484
337
|
|
|
485
|
-
|
|
338
|
+
Starts development server with hot reload:
|
|
486
339
|
|
|
487
|
-
|
|
340
|
+
```bash
|
|
341
|
+
ssx dev [options]
|
|
488
342
|
|
|
489
|
-
|
|
343
|
+
Options:
|
|
344
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
345
|
+
-p, --port <port> Dev server port (default: 3000)
|
|
346
|
+
```
|
|
490
347
|
|
|
491
|
-
|
|
492
|
-
import { trigger } from "@inglorious/store/test"
|
|
348
|
+
### Package.json Scripts
|
|
493
349
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"scripts": {
|
|
353
|
+
"dev": "ssx dev",
|
|
354
|
+
"build": "ssx build",
|
|
355
|
+
"preview": "ssx build && npx serve dist"
|
|
499
356
|
}
|
|
500
357
|
}
|
|
501
|
-
|
|
502
|
-
// Test it
|
|
503
|
-
const { entity, events } = trigger(
|
|
504
|
-
{ type: "counter", id: "counter1", value: 99 },
|
|
505
|
-
increment,
|
|
506
|
-
{ amount: 5 },
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
expect(entity.value).toBe(104)
|
|
510
|
-
expect(events).toEqual([{ type: "overflow", payload: { id: "counter1" } }])
|
|
511
358
|
```
|
|
512
359
|
|
|
513
|
-
|
|
360
|
+
---
|
|
514
361
|
|
|
515
|
-
|
|
362
|
+
## Project Structure
|
|
516
363
|
|
|
517
|
-
```
|
|
518
|
-
|
|
364
|
+
```
|
|
365
|
+
my-site/
|
|
366
|
+
├── src/
|
|
367
|
+
│ ├── pages/ # File-based routes
|
|
368
|
+
│ │ ├── index.js # Home page
|
|
369
|
+
│ │ ├── about.js # About page
|
|
370
|
+
│ │ └── posts/
|
|
371
|
+
│ │ ├── index.js # /posts
|
|
372
|
+
│ │ └── _id.js # /posts/:id
|
|
373
|
+
│ ├── entities.js # Entity definitions
|
|
374
|
+
│ └── types/ # Custom entity types (optional)
|
|
375
|
+
├── dist/ # Build output
|
|
376
|
+
├── package.json
|
|
377
|
+
└── vite.config.js # Optional Vite config
|
|
378
|
+
```
|
|
519
379
|
|
|
520
|
-
|
|
521
|
-
const api = createMockApi({
|
|
522
|
-
counter1: { type: "counter", value: 10 },
|
|
523
|
-
counter2: { type: "counter", value: 20 },
|
|
524
|
-
})
|
|
380
|
+
---
|
|
525
381
|
|
|
526
|
-
|
|
527
|
-
function copyValue(entity, payload, api) {
|
|
528
|
-
const source = api.getEntity(payload.sourceId)
|
|
529
|
-
entity.value = source.value
|
|
530
|
-
}
|
|
382
|
+
## Comparison to Other Tools
|
|
531
383
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
384
|
+
| Feature | SSX | Next.js (SSG) | Astro | Eleventy |
|
|
385
|
+
| ----------------------- | ----------- | ------------- | ------ | -------- |
|
|
386
|
+
| Pre-rendered HTML | ✅ | ✅ | ✅ | ✅ |
|
|
387
|
+
| Client hydration | ✅ lit-html | ✅ React | ✅ Any | ❌ |
|
|
388
|
+
| Client routing | ✅ | ✅ | ✅ | ❌ |
|
|
389
|
+
| Lazy loading | ✅ | ✅ | ✅ | ❌ |
|
|
390
|
+
| Entity-based state | ✅ | ❌ | ❌ | ❌ |
|
|
391
|
+
| No compilation required | ✅ | ❌ | ❌ | ✅ |
|
|
392
|
+
| Zero config | ✅ | ❌ | ❌ | ❌ |
|
|
393
|
+
| Framework agnostic | ❌ | ❌ | ✅ | ✅ |
|
|
539
394
|
|
|
540
|
-
|
|
541
|
-
|
|
395
|
+
SSX is perfect if you:
|
|
396
|
+
|
|
397
|
+
- Want static site performance
|
|
398
|
+
- Love entity-based architecture
|
|
399
|
+
- Prefer convention over configuration
|
|
400
|
+
- Need full client-side interactivity
|
|
401
|
+
- Don't want React/Vue lock-in
|
|
542
402
|
|
|
543
|
-
|
|
403
|
+
---
|
|
544
404
|
|
|
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.
|
|
405
|
+
## Advanced Usage
|
|
550
406
|
|
|
551
|
-
###
|
|
407
|
+
### Custom Vite Config
|
|
552
408
|
|
|
553
|
-
|
|
409
|
+
Extend the default Vite configuration:
|
|
554
410
|
|
|
555
411
|
```javascript
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
state.stats.total = allTodos.length
|
|
566
|
-
state.stats.completed = allTodos.filter((t) => t.completed).length
|
|
412
|
+
// vite.config.js
|
|
413
|
+
import { defineConfig } from "vite"
|
|
414
|
+
|
|
415
|
+
export default defineConfig({
|
|
416
|
+
// Your custom config
|
|
417
|
+
plugins: [],
|
|
418
|
+
resolve: {
|
|
419
|
+
alias: {
|
|
420
|
+
"@": "/src",
|
|
567
421
|
},
|
|
568
422
|
},
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const store = createStore({ types, entities, systems })
|
|
423
|
+
})
|
|
572
424
|
```
|
|
573
425
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
### 🔗 Behavior Composition
|
|
426
|
+
### Environment Variables
|
|
577
427
|
|
|
578
|
-
|
|
428
|
+
Use Vite's environment variables:
|
|
579
429
|
|
|
580
430
|
```javascript
|
|
581
|
-
//
|
|
582
|
-
const
|
|
583
|
-
increment(entity) {
|
|
584
|
-
entity.value++
|
|
585
|
-
},
|
|
586
|
-
|
|
587
|
-
decrement(entity) {
|
|
588
|
-
entity.value--
|
|
589
|
-
},
|
|
590
|
-
}
|
|
431
|
+
// Access in your code
|
|
432
|
+
const apiUrl = import.meta.env.VITE_API_URL
|
|
591
433
|
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
counter,
|
|
595
|
-
{
|
|
596
|
-
reset(entity) {
|
|
597
|
-
entity.value = 0
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
]
|
|
434
|
+
// .env file
|
|
435
|
+
VITE_API_URL=https://api.example.com
|
|
601
436
|
```
|
|
602
437
|
|
|
603
|
-
|
|
438
|
+
### Custom 404 Page
|
|
439
|
+
|
|
440
|
+
Create a fallback route:
|
|
604
441
|
|
|
605
442
|
```javascript
|
|
606
|
-
//
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
443
|
+
// src/pages/404.js
|
|
444
|
+
export const notFound = {
|
|
445
|
+
render() {
|
|
446
|
+
return html`
|
|
447
|
+
<div>
|
|
448
|
+
<h1>404 - Page Not Found</h1>
|
|
449
|
+
<a href="/">Go Home</a>
|
|
450
|
+
</div>
|
|
451
|
+
`
|
|
610
452
|
},
|
|
611
453
|
}
|
|
612
454
|
|
|
613
|
-
|
|
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]
|
|
455
|
+
export const title = "404"
|
|
632
456
|
```
|
|
633
457
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
### ⏱️ Batched Mode
|
|
637
|
-
|
|
638
|
-
The Inglorious Store features an **event queue**. In the default `auto` update mode, each notified event will trigger and update of the state (same as Redux). But in `manual` update mode, you can process multiple events together before re-rendering:
|
|
458
|
+
Register it in your router:
|
|
639
459
|
|
|
640
460
|
```javascript
|
|
641
|
-
|
|
461
|
+
// src/entities.js
|
|
462
|
+
import { setRoutes } from "@inglorious/web/router"
|
|
642
463
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// process them all in batch
|
|
649
|
-
store.update()
|
|
464
|
+
setRoutes({
|
|
465
|
+
// ... other routes
|
|
466
|
+
"*": "notFound", // Fallback
|
|
467
|
+
})
|
|
650
468
|
```
|
|
651
469
|
|
|
652
|
-
|
|
470
|
+
### Incremental Builds
|
|
653
471
|
|
|
654
|
-
|
|
472
|
+
Currently, SSX rebuilds all pages. For large sites, consider:
|
|
655
473
|
|
|
656
|
-
|
|
474
|
+
1. **Split into multiple deployments** - Blog vs. docs vs. marketing
|
|
475
|
+
2. **Use ISR-like patterns** - Rebuild changed pages via CI/CD
|
|
476
|
+
3. **Cache build artifacts** - Speed up unchanged pages
|
|
657
477
|
|
|
658
|
-
|
|
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 |
|
|
478
|
+
Incremental builds are planned for future releases.
|
|
668
479
|
|
|
669
480
|
---
|
|
670
481
|
|
|
671
482
|
## API Reference
|
|
672
483
|
|
|
673
|
-
###
|
|
484
|
+
### Build API
|
|
674
485
|
|
|
675
486
|
```javascript
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
487
|
+
import { build } from "@inglorious/ssx/build"
|
|
488
|
+
|
|
489
|
+
await build({
|
|
490
|
+
rootDir: "src", // Source directory
|
|
491
|
+
outDir: "dist", // Output directory
|
|
492
|
+
renderOptions: {
|
|
493
|
+
title: "My Site", // Default page title
|
|
494
|
+
meta: {}, // Default meta tags
|
|
495
|
+
styles: [], // Global CSS files
|
|
496
|
+
scripts: [], // Global JS files
|
|
497
|
+
},
|
|
681
498
|
})
|
|
682
499
|
```
|
|
683
500
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
### Types Definition
|
|
501
|
+
### Dev Server API
|
|
687
502
|
|
|
688
503
|
```javascript
|
|
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
|
-
],
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
### Event Handler API
|
|
710
|
-
|
|
711
|
-
Each handler receives three arguments:
|
|
504
|
+
import { dev } from "@inglorious/ssx/dev"
|
|
712
505
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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):
|
|
732
|
-
|
|
733
|
-
```javascript
|
|
734
|
-
store.notify("eventName", payload)
|
|
735
|
-
store.dispatch({ type: "eventName", payload }) // Redux-compatible alternative
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
### 🧩 Type Safety
|
|
739
|
-
|
|
740
|
-
Inglorious Store is written in JavaScript but comes with powerful TypeScript support out of the box, allowing for a fully type-safe experience similar to Redux Toolkit, but with less boilerplate.
|
|
741
|
-
|
|
742
|
-
You can achieve strong type safety by defining an interface for your `types` configuration. This allows you to statically define the shape of your entity handlers, ensuring that all required handlers are present and correctly typed.
|
|
743
|
-
|
|
744
|
-
Here’s how you can set it up for a TodoMVC-style application:
|
|
745
|
-
|
|
746
|
-
**1. Define Your Types**
|
|
747
|
-
|
|
748
|
-
First, create an interface that describes your entire `types` configuration. This interface will enforce the structure of your event handlers.
|
|
749
|
-
|
|
750
|
-
```typescript
|
|
751
|
-
// src/store/types.ts
|
|
752
|
-
import type {
|
|
753
|
-
FormEntity,
|
|
754
|
-
ListEntity,
|
|
755
|
-
FooterEntity,
|
|
756
|
-
// ... other payload types
|
|
757
|
-
} from "../../types"
|
|
758
|
-
|
|
759
|
-
// Define the static shape of the types configuration
|
|
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
|
-
}
|
|
774
|
-
|
|
775
|
-
export const types: TodoListTypes = {
|
|
776
|
-
form: {
|
|
777
|
-
inputChange(entity, value) {
|
|
778
|
-
entity.value = value
|
|
779
|
-
},
|
|
780
|
-
formSubmit(entity) {
|
|
781
|
-
entity.value = ""
|
|
782
|
-
},
|
|
506
|
+
await dev({
|
|
507
|
+
rootDir: "src",
|
|
508
|
+
port: 3000,
|
|
509
|
+
renderOptions: {
|
|
510
|
+
// ... same as build
|
|
783
511
|
},
|
|
784
|
-
// ... other type implementations
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
With `TodoListTypes`, TypeScript will throw an error if you forget a handler (e.g., `formSubmit`) or if its signature is incorrect.
|
|
789
|
-
|
|
790
|
-
**2. Create the Store**
|
|
791
|
-
|
|
792
|
-
When creating your store, you'll pass the `types` object. To satisfy the store's generic `TypesConfig`, you may need to use a double cast (`as unknown as`). This is a safe and intentional way to bridge your specific, statically-checked configuration with the store's more generic type.
|
|
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"
|
|
799
|
-
|
|
800
|
-
export const store = createStore<TodoListEntity, TodoListState>({
|
|
801
|
-
types: types as unknown as TypesConfig<TodoListEntity>,
|
|
802
|
-
// ... other store config
|
|
803
512
|
})
|
|
804
513
|
```
|
|
805
514
|
|
|
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
515
|
---
|
|
811
516
|
|
|
812
|
-
##
|
|
517
|
+
<!-- ## Examples
|
|
813
518
|
|
|
814
|
-
|
|
519
|
+
Check out these example projects:
|
|
815
520
|
|
|
816
|
-
-
|
|
817
|
-
-
|
|
818
|
-
-
|
|
819
|
-
-
|
|
820
|
-
- 🔄 Undo/redo, time-travel debugging
|
|
521
|
+
- **[Basic Blog](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-blog)** - Simple blog with posts
|
|
522
|
+
- **[Documentation Site](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-docs)** - Multi-page docs
|
|
523
|
+
- **[E-commerce](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-shop)** - Product catalog
|
|
524
|
+
- **[Portfolio](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/ssx-portfolio)** - Personal portfolio
|
|
821
525
|
|
|
822
|
-
|
|
526
|
+
--- -->
|
|
823
527
|
|
|
824
|
-
|
|
825
|
-
- Migration path from Redux (keep using react-redux)
|
|
528
|
+
## Roadmap
|
|
826
529
|
|
|
827
|
-
|
|
530
|
+
- [ ] TypeScript support
|
|
531
|
+
- [ ] Image optimization
|
|
532
|
+
- [ ] Incremental builds
|
|
533
|
+
- [ ] API routes (serverless functions)
|
|
534
|
+
- [ ] RSS feed generation
|
|
535
|
+
- [ ] Sitemap generation
|
|
536
|
+
- [ ] MDX support
|
|
537
|
+
- [ ] i18n helpers
|
|
828
538
|
|
|
829
|
-
|
|
539
|
+
---
|
|
830
540
|
|
|
831
|
-
|
|
541
|
+
## Philosophy
|
|
832
542
|
|
|
833
|
-
|
|
543
|
+
SSX embraces the philosophy of [@inglorious/web](https://www.npmjs.com/package/@inglorious/web):
|
|
834
544
|
|
|
835
|
-
- **
|
|
836
|
-
- **
|
|
837
|
-
- **
|
|
838
|
-
- **
|
|
545
|
+
- **Simplicity over cleverness** - Obvious beats clever
|
|
546
|
+
- **Convention over configuration** - Sensible defaults
|
|
547
|
+
- **Predictability over magic** - Explicit is better than implicit
|
|
548
|
+
- **Standards over abstractions** - Use the platform
|
|
839
549
|
|
|
840
|
-
|
|
550
|
+
Static site generation should be simple. SSX makes it simple.
|
|
841
551
|
|
|
842
552
|
---
|
|
843
553
|
|
|
844
|
-
##
|
|
845
|
-
|
|
846
|
-
It's hard to accept the new, especially on Reddit. Here are the main objections to the Inglorious Store.
|
|
554
|
+
## Contributing
|
|
847
555
|
|
|
848
|
-
|
|
556
|
+
Contributions are welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
|
849
557
|
|
|
850
|
-
|
|
558
|
+
---
|
|
851
559
|
|
|
852
|
-
|
|
560
|
+
## License
|
|
853
561
|
|
|
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 |
|
|
562
|
+
**MIT License** - Free and open source
|
|
861
563
|
|
|
862
|
-
|
|
564
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
863
565
|
|
|
864
|
-
|
|
566
|
+
---
|
|
865
567
|
|
|
866
|
-
|
|
568
|
+
## Related Packages
|
|
867
569
|
|
|
868
|
-
|
|
570
|
+
- [@inglorious/web](https://www.npmjs.com/package/@inglorious/web) - Entity-based web framework
|
|
571
|
+
- [@inglorious/store](https://www.npmjs.com/package/@inglorious/store) - State management
|
|
572
|
+
- [@inglorious/engine](https://www.npmjs.com/package/@inglorious/engine) - Game engine
|
|
869
573
|
|
|
870
574
|
---
|
|
871
575
|
|
|
872
|
-
##
|
|
873
|
-
|
|
874
|
-
MIT © [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
576
|
+
## Support
|
|
875
577
|
|
|
876
|
-
|
|
578
|
+
- 📖 [Documentation](https://inglorious-engine.vercel.app)
|
|
579
|
+
- 💬 [Discord Community](https://discord.gg/Byx85t2eFp)
|
|
580
|
+
- 🐛 [Issue Tracker](https://github.com/IngloriousCoderz/inglorious-forge/issues)
|
|
581
|
+
- 📧 [Email Support](mailto:antony.mistretta@gmail.com)
|
|
877
582
|
|
|
878
583
|
---
|
|
879
584
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
|
585
|
+
**Build static sites the Inglorious way. Simple. Predictable. Fast.** 🚀
|