@inglorious/web 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,8 @@ Available templates:
49
49
  - **minimal** — plain HTML, CSS, and JS (no build step)
50
50
  - **js** — Vite-based JavaScript project
51
51
  - **ts** — Vite + TypeScript project
52
+ - **ssx-js** — Static Site Xecution (SSX) project using JavaScript
53
+ - **ssx-ts** — Static Site Xecution (SSX) project using TypeScript
52
54
 
53
55
  Use the scaffolder to create a starter app tailored to your workflow.
54
56
 
@@ -83,6 +85,7 @@ It's that simple — and surprisingly fast in practice.
83
85
  - You want UI to be fully controlled by your entity-based store
84
86
  - You want to stay entirely in **JavaScript**, without DSLs or compilers
85
87
  - You want **React-like declarative UI** but without the cost and overhead of React
88
+ - You want to build **static sites with SSX** — same entity patterns, pre-rendered HTML, and client hydration
86
89
 
87
90
  This framework is ideal for both small apps and large business UIs.
88
91
 
@@ -90,7 +93,6 @@ This framework is ideal for both small apps and large business UIs.
90
93
 
91
94
  ## When NOT to Use Inglorious Web
92
95
 
93
- - You need server-side rendering (SSR) or static site generation (SSG) - WIP
94
96
  - You need fine-grained reactivity for very large datasets (1000+ items per view)
95
97
  - You're building a library that needs to be framework-agnostic
96
98
  - Your team is already deeply invested in React/Vue/Angular
@@ -367,12 +369,12 @@ No additional configuration is needed.
367
369
 
368
370
  ### 1. Setup the Router
369
371
 
370
- To enable the router, add it to your store's types and create a `router` entity. The entity's `routes` property maps URL patterns to the entity types that represent your pages.
372
+ To enable the router, add it to your store's types and create a `router` entity. Register route patterns using the router module helpers (`setRoutes`, `addRoute`) routes are configured at module level and not stored on the router entity itself.
371
373
 
372
374
  ```javascript
373
375
  // store.js
374
376
  import { createStore, html } from "@inglorious/web"
375
- import { router } from "@inglorious/web/router"
377
+ import { router, setRoutes } from "@inglorious/web/router"
376
378
 
377
379
  const types = {
378
380
  // 1. Add the router type to your store's types
@@ -395,14 +397,9 @@ const types = {
395
397
  }
396
398
 
397
399
  const entities = {
398
- // 3. Create the router entity
400
+ // 3. Create the router entity (no `routes` here)
399
401
  router: {
400
402
  type: "router",
401
- routes: {
402
- "/": "homePage",
403
- "/users/:id": "userPage",
404
- "*": "notFoundPage", // Fallback for unmatched routes
405
- },
406
403
  },
407
404
  userPage: {
408
405
  type: "userPage",
@@ -411,6 +408,13 @@ const entities = {
411
408
  }
412
409
 
413
410
  export const store = createStore({ types, entities })
411
+
412
+ // Register routes at module level
413
+ setRoutes({
414
+ "/": "homePage",
415
+ "/users/:id": "userPage",
416
+ "*": "notFoundPage",
417
+ })
414
418
  ```
415
419
 
416
420
  ### 2. Render the Current Route
@@ -456,22 +460,23 @@ api.notify("navigate", {
456
460
 
457
461
  ### 4. Lazy Loading Routes
458
462
 
459
- You can improve performance by lazy-loading routes. Instead of a string, provide a function that returns a dynamic import.
463
+ You can improve performance by lazy-loading routes. Use a loader function that returns a dynamic import when registering the route via `setRoutes`.
460
464
 
461
465
  **Note:** The imported module must use a named export for the entity type (not `export default`), so the router can register it with a unique name in the store.
462
466
 
463
467
  ```javascript
464
468
  // store.js
465
469
  const entities = {
466
- router: {
467
- type: "router",
468
- routes: {
469
- "/": "homePage",
470
- // Lazy load: returns a Promise resolving to a module
471
- "/admin": () => import("./pages/admin.js"),
472
- },
473
- },
470
+ router: { type: "router" },
474
471
  }
472
+
473
+ export const store = createStore({ types, entities })
474
+
475
+ setRoutes({
476
+ "/": "homePage",
477
+ // Lazy load: returns a Promise resolving to a module
478
+ "/admin": () => import("./pages/admin.js"),
479
+ })
475
480
  ```
476
481
 
477
482
  ```javascript
@@ -538,22 +543,18 @@ const types = {
538
543
  }
539
544
 
540
545
  const entities = {
541
- router: {
542
- type: "router",
543
- routes: {
544
- "/login": "loginPage",
545
- "/admin": "adminPage",
546
- },
547
- },
548
- adminPage: {
549
- type: "adminPage",
550
- },
551
- loginPage: {
552
- type: "loginPage",
553
- },
546
+ router: { type: "router" },
547
+ adminPage: { type: "adminPage" },
548
+ loginPage: { type: "loginPage" },
554
549
  }
555
550
 
556
551
  export const store = createStore({ types, entities })
552
+
553
+ // Register routes via the router module API
554
+ setRoutes({
555
+ "/login": "loginPage",
556
+ "/admin": "adminPage",
557
+ })
557
558
  ```
558
559
 
559
560
  #### How Type Composition Works
@@ -1029,6 +1030,23 @@ You can even mix them in the same app!
1029
1030
 
1030
1031
  ---
1031
1032
 
1033
+ ## Static Site Generation with SSX
1034
+
1035
+ For building **static HTML sites** with full pre-rendering, client-side hydration, and automatic sitemap/RSS generation, use [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx).
1036
+
1037
+ SSX is built entirely on **@inglorious/web** and lets you use the same entity-based patterns for both interactive apps and static sites, with:
1038
+
1039
+ - Pre-rendered HTML at build time
1040
+ - Automatic code splitting and lazy loading
1041
+ - Client-side hydration with lit-html
1042
+ - File-based routing
1043
+ - Sitemap and RSS feed generation
1044
+ - Incremental builds
1045
+
1046
+ It's the perfect companion to @inglorious/web for building blazing-fast static sites, blogs, documentation, and marketing pages.
1047
+
1048
+ ---
1049
+
1032
1050
  ## Examples
1033
1051
 
1034
1052
  Check out these demos to see `@inglorious/web` in action:
@@ -1042,6 +1060,15 @@ Check out these demos to see `@inglorious/web` in action:
1042
1060
 
1043
1061
  ---
1044
1062
 
1063
+ ## Related Packages
1064
+
1065
+ - [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx) - Static site generation with pre-rendering and client hydration
1066
+ - [**@inglorious/store**](https://www.npmjs.com/package/@inglorious/store) - Entity-based state management (used by @inglorious/web)
1067
+ - [**@inglorious/engine**](https://www.npmjs.com/package/@inglorious/engine) - Game engine with the same entity architecture
1068
+ - [**@inglorious/create-app**](https://www.npmjs.com/package/@inglorious/create-app) - Scaffolding tool for quick project setup
1069
+
1070
+ ---
1071
+
1045
1072
  ## License
1046
1073
 
1047
1074
  **MIT License - Free and open source**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -7,7 +7,18 @@
7
7
  const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
8
8
  const REMOVE_COLON_PREFIX = 1
9
9
 
10
+ /**
11
+ * Route configuration map. Keys are route patterns (e.g. `/users/:id`) and
12
+ * values are either a string type name or a loader function that returns a
13
+ * module exporting a type.
14
+ * @type {Record<string, string|function>}
15
+ */
10
16
  const routeConfig = {}
17
+
18
+ /**
19
+ * Guard to ensure global listeners are only attached once.
20
+ * @type {boolean}
21
+ */
11
22
  let areListenersInitialized = false
12
23
 
13
24
  /**
@@ -41,18 +52,9 @@ export const router = {
41
52
  areListenersInitialized = true
42
53
 
43
54
  // Listen for browser back/forward
44
- window.addEventListener("popstate", () => {
45
- const path = window.location.pathname + window.location.search
46
- const route = findRoute(routeConfig, path)
47
-
48
- if (route) {
49
- api.notify(`#${entityId}:routeSync`, {
50
- entityType: route.entityType,
51
- path,
52
- params: route.params,
53
- })
54
- }
55
- })
55
+ window.addEventListener("popstate", () =>
56
+ api.notify(`#${entityId}:popstate`, payload),
57
+ )
56
58
 
57
59
  // Intercept link clicks
58
60
  document.addEventListener("click", (event) => {
@@ -79,10 +81,44 @@ export const router = {
79
81
  event.preventDefault()
80
82
 
81
83
  const path = link.pathname + link.search + link.hash
82
- api.notify("navigate", path)
84
+ api.notify(`#${entityId}:navigate`, path)
83
85
  })
84
86
  },
85
87
 
88
+ /**
89
+ * Handles browser `popstate` events.
90
+ * Attempts to match the current location to a route and updates the router
91
+ * entity state. If the matched route is lazy (a loader function) it will
92
+ * attempt to load the module first.
93
+ * @param {RouterEntity} entity
94
+ * @param {Object} payload
95
+ * @param {Api} api
96
+ */
97
+ async popstate(entity, payload, api) {
98
+ const path = window.location.pathname + window.location.search
99
+ const route = findRoute(routeConfig, path)
100
+ const entityId = entity.id
101
+
102
+ if (route) {
103
+ if (typeof route.entityType === "function") {
104
+ entity.isLoading = true
105
+ entity.error = null
106
+
107
+ try {
108
+ const module = await route.entityType()
109
+ api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
110
+ api.notify(`#${entityId}:popstate`, payload)
111
+ } catch (error) {
112
+ api.notify(`#${entityId}:routeLoadError`, { error, path })
113
+ }
114
+
115
+ return
116
+ }
117
+
118
+ updateRouter(entity, route)
119
+ }
120
+ },
121
+
86
122
  /**
87
123
  * Handles navigation to a new route.
88
124
  * @param {RouterEntity} entity - The router entity.
@@ -113,6 +149,7 @@ export const router = {
113
149
  // The router will match it against patterns in routeConfig
114
150
 
115
151
  const route = findRoute(routeConfig, path)
152
+ const entityId = entity.id
116
153
 
117
154
  if (!route) {
118
155
  console.warn(`No route matches path: ${path}`)
@@ -132,64 +169,55 @@ export const router = {
132
169
  if (typeof route.entityType === "function") {
133
170
  entity.isLoading = true
134
171
  entity.error = null
135
- const entityId = entity.id
136
172
 
137
173
  try {
138
174
  const module = await route.entityType()
139
- api.notify(`#${entityId}:loadSuccess`, {
140
- module,
141
- route,
142
- path,
143
- replace,
144
- state,
145
- })
175
+ api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
176
+ api.notify(`#${entityId}:navigate`, payload)
146
177
  } catch (error) {
147
- api.notify(`#${entityId}:loadError`, { error, path })
178
+ api.notify(`#${entityId}:routeLoadError`, { error, path })
148
179
  }
149
180
 
150
181
  return
151
182
  }
152
183
 
153
- // Synchronous navigation
184
+ updateRouter(entity, route)
154
185
 
155
- doNavigate(
156
- entity,
157
- {
158
- entityType: route.entityType,
159
- path,
160
- params: route.params,
161
- replace,
162
- state,
163
- },
164
- api,
165
- )
186
+ // Prepare history state
187
+ const historyState = {
188
+ ...state,
189
+ route: entity.route,
190
+ params: entity.params,
191
+ query: entity.query,
192
+ path: entity.path,
193
+ }
194
+
195
+ // Navigate
196
+ const method = replace ? "replaceState" : "pushState"
197
+ history[method](historyState, "", path)
198
+
199
+ api.notify("routeChange", historyState)
166
200
  },
167
201
 
168
202
  /**
169
- * Handles the successful loading of a lazy route module.
170
- * @param {RouterEntity} entity - The router entity.
171
- * @param {{module: object, route: object, path: string, replace: boolean, state: object}} payload - The success payload.
172
- * @param {Api} api - The application API.
203
+ * Handles successful loading of a lazily-loaded route module.
204
+ * Registers the loaded type in the runtime type registry via `api.setType`
205
+ * and updates the `routeConfig` entry for the pattern.
206
+ * @param {RouterEntity} entity
207
+ * @param {{module: object, route: {pattern: string, entityType: string}}} payload
208
+ * @param {Api} api
173
209
  */
174
- loadSuccess(entity, payload, api) {
175
- const { module, route, path, replace, state } = payload
210
+ routeLoadSuccess(entity, payload, api) {
211
+ const { module, route } = payload
176
212
 
177
213
  const [typeName, type] = Object.entries(module).find(
178
214
  ([, type]) => type?.render,
179
215
  )
180
216
 
181
217
  api.setType(typeName, type)
182
-
183
218
  routeConfig[route.pattern] = typeName
184
219
 
185
- // Complete the navigation
186
220
  entity.isLoading = false
187
-
188
- doNavigate(
189
- entity,
190
- { entityType: typeName, path, params: route.params, replace, state },
191
- api,
192
- )
193
221
  },
194
222
 
195
223
  /**
@@ -197,22 +225,13 @@ export const router = {
197
225
  * @param {RouterEntity} entity - The router entity.
198
226
  * @param {{error: Error, path: string}} payload - The error payload.
199
227
  */
200
- loadError(entity, payload) {
228
+ routeLoadError(entity, payload) {
201
229
  const { error, path } = payload
202
230
  console.error(`Failed to load route ${path}:`, error)
203
231
  entity.path = path
204
232
  entity.isLoading = false
205
233
  entity.error = error
206
234
  },
207
-
208
- /**
209
- * Synchronizes the router state with the browser's history (e.g., on popstate).
210
- * @param {RouterEntity} entity - The router entity.
211
- * @param {{entityType: string, path: string, params: object}} payload - The sync payload.
212
- */
213
- routeSync(entity, payload) {
214
- updateRouter(entity, payload)
215
- },
216
235
  }
217
236
 
218
237
  /**
@@ -278,24 +297,29 @@ function buildPath(pattern, params = {}) {
278
297
  * Finds a matching route configuration for a given URL path.
279
298
  * It supports parameterized routes and a fallback "*" route.
280
299
  * @param {Record<string, string>} routeConfig - The routes configuration map.
281
- * @param {string} path - The URL path to match.
300
+ * @param {string} pathname - The URL path to match.
282
301
  * @returns {{pattern: string, entityType: string, params: Record<string, string>, path: string}|null}
283
302
  * The matched route object or null if no match is found.
284
303
  */
285
- function findRoute(routeConfig, path) {
286
- const [pathname] = path.split("?")
304
+ function findRoute(routeConfig, pathname) {
305
+ const [path, search] = pathname.split("?")
287
306
  let fallbackRoute = null
288
307
 
289
308
  for (const [pattern, entityType] of Object.entries(routeConfig)) {
290
309
  if (pattern === "*") {
291
- fallbackRoute = { pattern, entityType, params: {}, path: pathname }
310
+ fallbackRoute = { pattern, entityType, params: {}, path }
292
311
  continue
293
312
  }
294
- const params = matchRoute(pattern, pathname)
313
+
314
+ const params = matchRoute(pattern, path)
295
315
  if (params !== null) {
296
- return { pattern, entityType, params, path: pathname }
316
+ const query = search
317
+ ? Object.fromEntries(new URLSearchParams(search))
318
+ : {}
319
+ return { pattern, entityType, params, path, query }
297
320
  }
298
321
  }
322
+
299
323
  return fallbackRoute
300
324
  }
301
325
 
@@ -345,50 +369,18 @@ function patternToRegex(pattern) {
345
369
  return new RegExp(`^${regexPattern}$`)
346
370
  }
347
371
 
348
- /**
349
- * Performs the actual navigation by updating entity state and browser history.
350
- * @param {RouterEntity} entity - The router entity.
351
- * @param {object} options - Navigation options.
352
- * @param {string} options.entityType - The type of the entity to render.
353
- * @param {string} options.path - The full path.
354
- * @param {object} options.params - The route parameters.
355
- * @param {boolean} [options.replace] - Whether to replace the current history entry.
356
- * @param {object} [options.state] - Additional state to save in history.
357
- * @param {Api} api - The application API.
358
- */
359
- function doNavigate(entity, { entityType, path, params, replace, state }, api) {
360
- updateRouter(entity, { entityType, path, params })
361
-
362
- // Prepare history state
363
- const historyState = {
364
- ...state,
365
- route: entity.route,
366
- params: entity.params,
367
- query: entity.query,
368
- path: entity.path,
369
- }
370
-
371
- // Navigate
372
- const method = replace ? "replaceState" : "pushState"
373
- history[method](historyState, "", path)
374
-
375
- api.notify("routeChange", historyState)
376
- }
377
-
378
372
  /**
379
373
  * Updates the router entity's internal state.
380
374
  * @param {RouterEntity} entity - The router entity.
381
375
  * @param {object} options - The update options.
382
376
  * @param {string} options.entityType - The matched entity type.
383
- * @param {string} options.path - The full path.
384
- * @param {object} options.params - The extracted parameters.
377
+ * @param {string} options.path - The full path (pathname only, no query).
378
+ * @param {object} options.params - The extracted route parameters.
379
+ * @param {object} [options.query] - The parsed query parameters.
385
380
  */
386
- function updateRouter(entity, { entityType, path, params }) {
387
- const [pathname, search] = path.split("?")
388
- const query = search ? Object.fromEntries(new URLSearchParams(search)) : {}
389
-
390
- entity.path = pathname
381
+ function updateRouter(entity, { entityType, path, params, query }) {
391
382
  entity.route = entityType
383
+ entity.path = path
392
384
  entity.params = params
393
385
  entity.query = query
394
386
  entity.hash = window.location.hash
package/types/router.d.ts CHANGED
@@ -114,21 +114,25 @@ export interface RouterType {
114
114
  payload: RouteSyncPayload,
115
115
  api: StoreApi,
116
116
  ): void
117
+ /**
118
+ * Handles browser `popstate` events. May perform async loading for lazy routes.
119
+ */
120
+ popstate(
121
+ entity: RouterEntity,
122
+ payload: any,
123
+ api: StoreApi,
124
+ ): void | Promise<void>
117
125
 
118
126
  /**
119
- * Handles successful async route loading.
120
- * @param {RouterEntity} entity - The router entity to update.
121
- * @param {Object} payload - The load success payload.
122
- * @param {StoreApi} api - The store API.
127
+ * Handles successful async route loading for a pattern.
128
+ * Registers the loaded type and updates runtime route config.
123
129
  */
124
- loadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
130
+ routeLoadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
125
131
 
126
132
  /**
127
- * Handles async route loading errors.
128
- * @param {RouterEntity} entity - The router entity.
129
- * @param {Object} payload - The error payload.
133
+ * Handles errors that occurred while loading a lazy route.
130
134
  */
131
- loadError(entity: RouterEntity, payload: any): void
135
+ routeLoadError(entity: RouterEntity, payload: any): void
132
136
  }
133
137
 
134
138
  /**