@inglorious/ssx 0.1.1

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/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@inglorious/ssx",
3
+ "version": "0.1.1",
4
+ "description": "Server-Side-X. Xecution? Xperience? Who knows.",
5
+ "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/IngloriousCoderz/inglorious-forge.git",
10
+ "directory": "packages/store"
11
+ },
12
+ "homepage": "https://inglorious-engine.vercel.app/",
13
+ "bugs": {
14
+ "url": "https://github.com/IngloriousCoderz/inglorious-forge/issues"
15
+ },
16
+ "keywords": [
17
+ "ssg",
18
+ "ssr",
19
+ "server",
20
+ "inglorious",
21
+ "web",
22
+ "framework"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./types/index.d.ts",
28
+ "import": "./src/store.js"
29
+ },
30
+ "./*": {
31
+ "types": "./types/*.d.ts",
32
+ "import": "./src/*"
33
+ }
34
+ },
35
+ "files": [
36
+ "src"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "happy-dom": "^20.0.11",
43
+ "@inglorious/web": "2.4.0"
44
+ },
45
+ "devDependencies": {
46
+ "prettier": "^3.6.2",
47
+ "vite": "^7.1.3",
48
+ "vitest": "^1.6.1",
49
+ "@inglorious/eslint-config": "1.1.1"
50
+ },
51
+ "engines": {
52
+ "node": ">= 22"
53
+ },
54
+ "scripts": {
55
+ "format": "prettier --write '**/*.{js,jsx}'",
56
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
57
+ "test:watch": "vitest",
58
+ "test": "vitest run"
59
+ }
60
+ }
@@ -0,0 +1,193 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`toHTML > API rendering within components > should support api.render() method in component render function 1`] = `"<div><div><span>Test Item</span></div></div>"`;
4
+
5
+ exports[`toHTML > HTML wrapping > should default to empty title when not provided 1`] = `
6
+ "<!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <title></title>
11
+
12
+
13
+ </head>
14
+ <body>
15
+ <div id="root"><p>Content</p></div>
16
+
17
+ </body>
18
+ </html>"
19
+ `;
20
+
21
+ exports[`toHTML > HTML wrapping > should handle empty arrays for metas, styles, and scripts 1`] = `
22
+ "<!DOCTYPE html>
23
+ <html>
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <title></title>
27
+
28
+
29
+ </head>
30
+ <body>
31
+ <div id="root"><p>Content</p></div>
32
+
33
+ </body>
34
+ </html>"
35
+ `;
36
+
37
+ exports[`toHTML > HTML wrapping > should include all options in wrapped HTML 1`] = `
38
+ "<!DOCTYPE html>
39
+ <html>
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <title>Complete Page</title>
43
+ <meta name="author" content="Test Author">
44
+ <link rel="stylesheet" href="/style.css">
45
+ </head>
46
+ <body>
47
+ <div id="root"><main>Main content</main></div>
48
+ <script type="module" src="/app.js"></script>
49
+ </body>
50
+ </html>"
51
+ `;
52
+
53
+ exports[`toHTML > HTML wrapping > should include meta tags in wrapped HTML 1`] = `
54
+ "<!DOCTYPE html>
55
+ <html>
56
+ <head>
57
+ <meta charset="UTF-8">
58
+ <title>Test Page</title>
59
+ <meta name="description" content="Test description">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1">
61
+
62
+ </head>
63
+ <body>
64
+ <div id="root"><p>Content</p></div>
65
+
66
+ </body>
67
+ </html>"
68
+ `;
69
+
70
+ exports[`toHTML > HTML wrapping > should include scripts in wrapped HTML 1`] = `
71
+ "<!DOCTYPE html>
72
+ <html>
73
+ <head>
74
+ <meta charset="UTF-8">
75
+ <title></title>
76
+
77
+
78
+ </head>
79
+ <body>
80
+ <div id="root"><p>Content</p></div>
81
+ <script type="module" src="/js/app.js"></script>
82
+ <script type="module" src="/js/analytics.js"></script>
83
+ </body>
84
+ </html>"
85
+ `;
86
+
87
+ exports[`toHTML > HTML wrapping > should include stylesheets in wrapped HTML 1`] = `
88
+ "<!DOCTYPE html>
89
+ <html>
90
+ <head>
91
+ <meta charset="UTF-8">
92
+ <title></title>
93
+
94
+ <link rel="stylesheet" href="/css/style.css">
95
+ <link rel="stylesheet" href="/css/theme.css">
96
+ </head>
97
+ <body>
98
+ <div id="root"><p>Content</p></div>
99
+
100
+ </body>
101
+ </html>"
102
+ `;
103
+
104
+ exports[`toHTML > HTML wrapping > should wrap HTML with basic DOCTYPE and structure 1`] = `
105
+ "<!DOCTYPE html>
106
+ <html>
107
+ <head>
108
+ <meta charset="UTF-8">
109
+ <title>My Page</title>
110
+
111
+
112
+ </head>
113
+ <body>
114
+ <div id="root"><h1>Page Title</h1></div>
115
+
116
+ </body>
117
+ </html>"
118
+ `;
119
+
120
+ exports[`toHTML > basic rendering > should render empty content 1`] = `""`;
121
+
122
+ exports[`toHTML > basic rendering > should render nested elements 1`] = `
123
+ "<div class="container">
124
+ <h1>Title</h1>
125
+ <p>Content</p>
126
+ </div>"
127
+ `;
128
+
129
+ exports[`toHTML > basic rendering > should render simple HTML without wrapping 1`] = `"<h1>Hello World</h1>"`;
130
+
131
+ exports[`toHTML > basic rendering > should render with inline styles 1`] = `"<div style="color: red; font-size: 16px;">Styled</div>"`;
132
+
133
+ exports[`toHTML > complex scenarios > should render a complete page structure with message list 1`] = `
134
+ "<div class="app">
135
+ <header><h1>Messages</h1></header>
136
+ <main><div class="message"><p>First message</p></div> <div class="message"><p>Second message</p></div></main>
137
+ <footer>© 2024</footer>
138
+ </div>"
139
+ `;
140
+
141
+ exports[`toHTML > complex scenarios > should render wrapped complex page with all assets 1`] = `
142
+ "<!DOCTYPE html>
143
+ <html>
144
+ <head>
145
+ <meta charset="UTF-8">
146
+ <title>My Website</title>
147
+ <meta name="description" content="Welcome to my site">
148
+ <meta name="viewport" content="width=device-width">
149
+ <link rel="stylesheet" href="/style.css">
150
+ </head>
151
+ <body>
152
+ <div id="root"><div>
153
+ <header><h1>My Website</h1></header>
154
+ <p>Welcome!</p>
155
+ </div></div>
156
+ <script type="module" src="/script.js"></script>
157
+ </body>
158
+ </html>"
159
+ `;
160
+
161
+ exports[`toHTML > edge cases > should handle special characters in content 1`] = `"<p>&lt;script&gt; &amp; "quotes"</p>"`;
162
+
163
+ exports[`toHTML > edge cases > should not include wrap by default 1`] = `"<p>Content</p>"`;
164
+
165
+ exports[`toHTML > edge cases > should return only inner HTML when wrap is false 1`] = `"<p>Inner</p>"`;
166
+
167
+ exports[`toHTML > event handling > should render event handlers in templates 1`] = `
168
+ "<div><div>
169
+ Click me
170
+ </div></div>"
171
+ `;
172
+
173
+ exports[`toHTML > event handling > should render multiple event handlers 1`] = `
174
+ "<div><div>
175
+ <button>
176
+ +
177
+ </button>
178
+ <span>5</span>
179
+ <button>
180
+ -
181
+ </button>
182
+ </div></div>"
183
+ `;
184
+
185
+ exports[`toHTML > rendering with state > should evaluate conditional rendering based on state 1`] = `"<div><p>Visible content</p></div>"`;
186
+
187
+ exports[`toHTML > rendering with state > should render entities from store 1`] = `"<div><span>Hello from store</span></div>"`;
188
+
189
+ exports[`toHTML > rendering with state > should render multiple entities 1`] = `
190
+ "<ul>
191
+ <li>First</li> <li>Second</li> <li>Third</li>
192
+ </ul>"
193
+ `;
package/src/html.js ADDED
@@ -0,0 +1,39 @@
1
+ import { mount } from "@inglorious/web"
2
+ import { Window } from "happy-dom"
3
+
4
+ export function toHTML(store, renderFn, options = {}) {
5
+ const window = new Window()
6
+ const document = window.document
7
+ document.body.innerHTML = '<div id="root"></div>'
8
+
9
+ const root = document.getElementById("root")
10
+ mount(store, renderFn, root)
11
+
12
+ const html = stripLitMarkers(root.innerHTML)
13
+ window.close()
14
+
15
+ return options.wrap ? wrapHTML(html, options) : html
16
+ }
17
+
18
+ function stripLitMarkers(html) {
19
+ return html
20
+ .replace(/<!--\?[^>]*-->/g, "") // All lit-html markers
21
+ .replace(/<!--\s*-->/g, "") // Empty comments
22
+ }
23
+
24
+ function wrapHTML(body, options) {
25
+ const { title = "", metas = [], styles = [], scripts = [] } = options
26
+ return `<!DOCTYPE html>
27
+ <html>
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <title>${title}</title>
31
+ ${metas.map((meta) => `<meta name="${meta.name}" content="${meta.content}">`).join("\n")}
32
+ ${styles.map((href) => `<link rel="stylesheet" href="${href}">`).join("\n")}
33
+ </head>
34
+ <body>
35
+ <div id="root">${body}</div>
36
+ ${scripts.map((src) => `<script type="module" src="${src}"></script>`).join("\n")}
37
+ </body>
38
+ </html>`
39
+ }
@@ -0,0 +1,375 @@
1
+ import { createStore, html } from "@inglorious/web"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { toHTML } from "./html.js"
5
+
6
+ describe("toHTML", () => {
7
+ describe("basic rendering", () => {
8
+ it("should render simple HTML without wrapping", () => {
9
+ const store = createStore()
10
+ const renderFn = () => html`<h1>Hello World</h1>`
11
+
12
+ const result = toHTML(store, renderFn)
13
+
14
+ expect(result).toMatchSnapshot()
15
+ })
16
+
17
+ it("should render empty content", () => {
18
+ const store = createStore()
19
+ const renderFn = () => html``
20
+
21
+ const result = toHTML(store, renderFn)
22
+
23
+ expect(result).toMatchSnapshot()
24
+ })
25
+
26
+ it("should render nested elements", () => {
27
+ const store = createStore()
28
+ const renderFn = () =>
29
+ html`<div class="container">
30
+ <h1>Title</h1>
31
+ <p>Content</p>
32
+ </div>`
33
+
34
+ const result = toHTML(store, renderFn)
35
+
36
+ expect(result).toMatchSnapshot()
37
+ })
38
+
39
+ it("should render with inline styles", () => {
40
+ const store = createStore()
41
+ const renderFn = () =>
42
+ html`<div style="color: red; font-size: 16px;">Styled</div>`
43
+
44
+ const result = toHTML(store, renderFn)
45
+
46
+ expect(result).toMatchSnapshot()
47
+ })
48
+ })
49
+
50
+ describe("rendering with state", () => {
51
+ it("should render entities from store", () => {
52
+ const store = createStore({
53
+ types: {
54
+ message: {
55
+ render: (entity) => html`<span>${entity.text}</span>`,
56
+ },
57
+ },
58
+ entities: {
59
+ greeting: { type: "message", text: "Hello from store" },
60
+ },
61
+ })
62
+
63
+ const renderFn = (api) => html`<div>${api.render("greeting")}</div>`
64
+
65
+ const result = toHTML(store, renderFn)
66
+
67
+ expect(result).toMatchSnapshot()
68
+ })
69
+
70
+ it("should render multiple entities", () => {
71
+ const store = createStore({
72
+ types: {
73
+ item: {
74
+ render: (entity) => html`<li>${entity.name}</li>`,
75
+ },
76
+ },
77
+ entities: {
78
+ item1: { type: "item", name: "First" },
79
+ item2: { type: "item", name: "Second" },
80
+ item3: { type: "item", name: "Third" },
81
+ },
82
+ })
83
+
84
+ const renderFn = (api) =>
85
+ html`<ul>
86
+ ${api.render("item1")} ${api.render("item2")} ${api.render("item3")}
87
+ </ul>`
88
+
89
+ const result = toHTML(store, renderFn)
90
+
91
+ expect(result).toMatchSnapshot()
92
+ })
93
+
94
+ it("should evaluate conditional rendering based on state", () => {
95
+ const store = createStore({
96
+ types: {
97
+ content: {
98
+ render: (entity) =>
99
+ entity.isVisible
100
+ ? html`<p>Visible content</p>`
101
+ : html`<p>Hidden</p>`,
102
+ },
103
+ },
104
+ entities: {
105
+ content: { type: "content", isVisible: true },
106
+ },
107
+ })
108
+
109
+ const renderFn = (api) => html`<div>${api.render("content")}</div>`
110
+
111
+ const result = toHTML(store, renderFn)
112
+
113
+ expect(result).toMatchSnapshot()
114
+ })
115
+ })
116
+
117
+ describe("HTML wrapping", () => {
118
+ it("should wrap HTML with basic DOCTYPE and structure", () => {
119
+ const store = createStore()
120
+ const renderFn = () => html`<h1>Page Title</h1>`
121
+
122
+ const result = toHTML(store, renderFn, { wrap: true, title: "My Page" })
123
+
124
+ expect(result).toMatchSnapshot()
125
+ })
126
+
127
+ it("should include meta tags in wrapped HTML", () => {
128
+ const store = createStore()
129
+ const renderFn = () => html`<p>Content</p>`
130
+
131
+ const result = toHTML(store, renderFn, {
132
+ wrap: true,
133
+ title: "Test Page",
134
+ metas: [
135
+ { name: "description", content: "Test description" },
136
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
137
+ ],
138
+ })
139
+
140
+ expect(result).toMatchSnapshot()
141
+ })
142
+
143
+ it("should include stylesheets in wrapped HTML", () => {
144
+ const store = createStore()
145
+ const renderFn = () => html`<p>Content</p>`
146
+
147
+ const result = toHTML(store, renderFn, {
148
+ wrap: true,
149
+ styles: ["/css/style.css", "/css/theme.css"],
150
+ })
151
+
152
+ expect(result).toMatchSnapshot()
153
+ })
154
+
155
+ it("should include scripts in wrapped HTML", () => {
156
+ const store = createStore()
157
+ const renderFn = () => html`<p>Content</p>`
158
+
159
+ const result = toHTML(store, renderFn, {
160
+ wrap: true,
161
+ scripts: ["/js/app.js", "/js/analytics.js"],
162
+ })
163
+
164
+ expect(result).toMatchSnapshot()
165
+ })
166
+
167
+ it("should include all options in wrapped HTML", () => {
168
+ const store = createStore()
169
+ const renderFn = () => html`<main>Main content</main>`
170
+
171
+ const result = toHTML(store, renderFn, {
172
+ wrap: true,
173
+ title: "Complete Page",
174
+ metas: [{ name: "author", content: "Test Author" }],
175
+ styles: ["/style.css"],
176
+ scripts: ["/app.js"],
177
+ })
178
+
179
+ expect(result).toMatchSnapshot()
180
+ })
181
+
182
+ it("should default to empty title when not provided", () => {
183
+ const store = createStore()
184
+ const renderFn = () => html`<p>Content</p>`
185
+
186
+ const result = toHTML(store, renderFn, { wrap: true })
187
+
188
+ expect(result).toMatchSnapshot()
189
+ })
190
+
191
+ it("should handle empty arrays for metas, styles, and scripts", () => {
192
+ const store = createStore()
193
+ const renderFn = () => html`<p>Content</p>`
194
+
195
+ const result = toHTML(store, renderFn, {
196
+ wrap: true,
197
+ metas: [],
198
+ styles: [],
199
+ scripts: [],
200
+ })
201
+
202
+ expect(result).toMatchSnapshot()
203
+ })
204
+ })
205
+
206
+ describe("API rendering within components", () => {
207
+ it("should support api.render() method in component render function", () => {
208
+ const store = createStore({
209
+ types: {
210
+ wrapper: {
211
+ render: (entity, api) => html`<div>${api.render("myItem")}</div>`,
212
+ },
213
+ item: { render: (entity) => html`<span>${entity.label}</span>` },
214
+ },
215
+ entities: {
216
+ myWrapper: { type: "wrapper" },
217
+ myItem: { type: "item", label: "Test Item" },
218
+ },
219
+ })
220
+
221
+ const renderFn = (api) => html`<div>${api.render("myWrapper")}</div>`
222
+
223
+ const result = toHTML(store, renderFn)
224
+
225
+ expect(result).toMatchSnapshot()
226
+ })
227
+ })
228
+
229
+ describe("complex scenarios", () => {
230
+ it("should render a complete page structure with message list", () => {
231
+ const store = createStore({
232
+ types: {
233
+ message: {
234
+ render: (entity) =>
235
+ html`<div class="message"><p>${entity.text}</p></div>`,
236
+ },
237
+ },
238
+ entities: {
239
+ msg1: { type: "message", text: "First message" },
240
+ msg2: { type: "message", text: "Second message" },
241
+ },
242
+ })
243
+
244
+ const renderFn = (api) =>
245
+ html`<div class="app">
246
+ <header><h1>Messages</h1></header>
247
+ <main>${api.render("msg1")} ${api.render("msg2")}</main>
248
+ <footer>© 2024</footer>
249
+ </div>`
250
+
251
+ const result = toHTML(store, renderFn)
252
+
253
+ expect(result).toMatchSnapshot()
254
+ })
255
+
256
+ it("should render wrapped complex page with all assets", () => {
257
+ const store = createStore({
258
+ types: {
259
+ header: { render: () => html`<header><h1>My Website</h1></header>` },
260
+ },
261
+ entities: { header: { type: "header" } },
262
+ })
263
+
264
+ const renderFn = (api) =>
265
+ html`<div>
266
+ ${api.render("header")}
267
+ <p>Welcome!</p>
268
+ </div>`
269
+
270
+ const result = toHTML(store, renderFn, {
271
+ wrap: true,
272
+ title: "My Website",
273
+ metas: [
274
+ { name: "description", content: "Welcome to my site" },
275
+ { name: "viewport", content: "width=device-width" },
276
+ ],
277
+ styles: ["/style.css"],
278
+ scripts: ["/script.js"],
279
+ })
280
+
281
+ expect(result).toMatchSnapshot()
282
+ })
283
+ })
284
+
285
+ describe("event handling", () => {
286
+ it("should render event handlers in templates", () => {
287
+ const store = createStore({
288
+ types: {
289
+ button: {
290
+ render: (entity, api) =>
291
+ html`<div @click=${() => api.notify(`#${entity.id}:click`)}>
292
+ Click me
293
+ </div>`,
294
+ },
295
+ },
296
+ entities: {
297
+ myButton: { type: "button", id: "myButton" },
298
+ },
299
+ })
300
+
301
+ const renderFn = (api) => html`<div>${api.render("myButton")}</div>`
302
+
303
+ const result = toHTML(store, renderFn)
304
+
305
+ expect(result).toMatchSnapshot()
306
+ })
307
+
308
+ it("should render multiple event handlers", () => {
309
+ const store = createStore({
310
+ types: {
311
+ counter: {
312
+ render: (entity, api) =>
313
+ html`<div>
314
+ <button @click=${() => api.notify(`#${entity.id}:increment`)}>
315
+ +
316
+ </button>
317
+ <span>${entity.count}</span>
318
+ <button @click=${() => api.notify(`#${entity.id}:decrement`)}>
319
+ -
320
+ </button>
321
+ </div>`,
322
+ },
323
+ },
324
+ entities: {
325
+ counter1: { type: "counter", id: "counter1", count: 5 },
326
+ },
327
+ })
328
+
329
+ const renderFn = (api) => html`<div>${api.render("counter1")}</div>`
330
+
331
+ const result = toHTML(store, renderFn)
332
+
333
+ expect(result).toMatchSnapshot()
334
+ })
335
+ })
336
+
337
+ describe("edge cases", () => {
338
+ it("should handle special characters in content", () => {
339
+ const store = createStore()
340
+ const renderFn = () => html`<p>&lt;script&gt; &amp; "quotes"</p>`
341
+
342
+ const result = toHTML(store, renderFn)
343
+
344
+ expect(result).toMatchSnapshot()
345
+ })
346
+
347
+ it("should not include wrap by default", () => {
348
+ const store = createStore()
349
+ const renderFn = () => html`<p>Content</p>`
350
+
351
+ const result = toHTML(store, renderFn, {})
352
+
353
+ expect(result).toMatchSnapshot()
354
+ })
355
+
356
+ it("should return only inner HTML when wrap is false", () => {
357
+ const store = createStore()
358
+ const renderFn = () => html`<p>Inner</p>`
359
+
360
+ const result = toHTML(store, renderFn, { wrap: false })
361
+
362
+ expect(result).toMatchSnapshot()
363
+ })
364
+
365
+ it("should close DOM window properly", () => {
366
+ const store = createStore()
367
+ const renderFn = () => html`<p>Test</p>`
368
+
369
+ const result = toHTML(store, renderFn)
370
+
371
+ expect(result).toBeDefined()
372
+ expect(result).not.toBeNull()
373
+ })
374
+ })
375
+ })