@inglorious/ssx 0.3.0 → 0.4.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/README.md +2 -3
- package/bin/ssx.js +2 -48
- package/package.json +3 -3
- package/src/build.js +14 -25
- package/src/dev.js +4 -14
- package/src/html.js +41 -14
- package/src/html.test.js +50 -47
- package/src/render.js +35 -18
- package/src/render.test.js +14 -10
- package/src/router.js +30 -29
- package/src/scripts/app.js +29 -12
- package/src/vite-config.js +9 -3
- package/src/scripts/lit-loader.js +0 -23
- package/src/scripts/main.js +0 -5
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Game engines solved state complexity years ago — Inglorious Store brings those
|
|
|
30
30
|
- ✅ Entity-based state (manage multiple instances effortlessly)
|
|
31
31
|
- ✅ No action creators, thunks, or slices
|
|
32
32
|
- ✅ Predictable, testable, purely functional code
|
|
33
|
-
- ✅ Built-in lifecycle events (`add`, `remove
|
|
33
|
+
- ✅ Built-in lifecycle events (`add`, `remove`)
|
|
34
34
|
- ✅ 10x faster immutability than Redux Toolkit (Mutative vs Immer)
|
|
35
35
|
|
|
36
36
|
---
|
|
@@ -363,7 +363,6 @@ Inglorious Store has a few built-in events that you can use:
|
|
|
363
363
|
|
|
364
364
|
- `add`: adds a new entity to the state. Triggers a `create` lifecycle event.
|
|
365
365
|
- `remove`: removes an entity from the state. Triggers a `destroy` lifecycle event.
|
|
366
|
-
- `morph`: changes the behavior of a type (advanced, used by middlewares/rendering systems)
|
|
367
366
|
|
|
368
367
|
The lifecycle events can be used to define event handlers similar to constructor and destructor methods in OOP:
|
|
369
368
|
|
|
@@ -720,12 +719,12 @@ Each handler receives three arguments:
|
|
|
720
719
|
- `dispatch(action)` - optional, if you prefer Redux-style dispatching
|
|
721
720
|
- `getTypes()` - type definitions (for middleware)
|
|
722
721
|
- `getType(typeName)` - type definition (for overriding)
|
|
722
|
+
- `setType(typeName, type)` - change the behavior of a type
|
|
723
723
|
|
|
724
724
|
### Built-in Events
|
|
725
725
|
|
|
726
726
|
- **`create(entity)`** - triggered when entity added via `add` event, visible only to that entity
|
|
727
727
|
- **`destroy(entity)`** - triggered when entity removed via `remove` event, visible only to that entity
|
|
728
|
-
- **`morph(typeName, newType)`** - used to change the behavior of a type on the fly
|
|
729
728
|
|
|
730
729
|
### Notify vs Dispatch
|
|
731
730
|
|
package/bin/ssx.js
CHANGED
|
@@ -4,9 +4,9 @@ import path from "node:path"
|
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
5
|
|
|
6
6
|
import { Command } from "commander"
|
|
7
|
-
import { Window } from "happy-dom"
|
|
8
7
|
|
|
9
|
-
import {
|
|
8
|
+
import { build } from "../src/build.js"
|
|
9
|
+
import { dev } from "../src/dev.js"
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
12
|
const __dirname = path.dirname(__filename)
|
|
@@ -28,40 +28,17 @@ program
|
|
|
28
28
|
.description("Start development server with hot reload")
|
|
29
29
|
.option("-r, --root <dir>", "source root directory", "src")
|
|
30
30
|
.option("-p, --port <port>", "dev server port", 3000)
|
|
31
|
-
.option("-s, --seed <seed>", "seed for random number generator", 42)
|
|
32
31
|
.option("-t, --title <title>", "default page title", "My Site")
|
|
33
32
|
.option("--styles <styles...>", "CSS files to include")
|
|
34
33
|
.option("--scripts <scripts...>", "JS files to include")
|
|
35
34
|
.action(async (options) => {
|
|
36
35
|
const cwd = process.cwd()
|
|
37
|
-
const seed = Number(options.seed)
|
|
38
36
|
|
|
39
37
|
try {
|
|
40
|
-
// 1️⃣ Install DOM *before anything else*
|
|
41
|
-
const window = new Window()
|
|
42
|
-
|
|
43
|
-
globalThis.window = window
|
|
44
|
-
globalThis.document = window.document
|
|
45
|
-
globalThis.HTMLElement = window.HTMLElement
|
|
46
|
-
globalThis.Node = window.Node
|
|
47
|
-
globalThis.Comment = window.Comment
|
|
48
|
-
|
|
49
|
-
// Optional but sometimes needed
|
|
50
|
-
globalThis.customElements = window.customElements
|
|
51
|
-
|
|
52
|
-
// 3️⃣ Patch with the parsed seed
|
|
53
|
-
const restore = patchRandom(seed)
|
|
54
|
-
await import("@inglorious/web")
|
|
55
|
-
restore()
|
|
56
|
-
|
|
57
|
-
// 4️⃣ NOW import and run dev
|
|
58
|
-
const { dev } = await import("../src/dev.js")
|
|
59
|
-
|
|
60
38
|
await dev({
|
|
61
39
|
rootDir: path.resolve(cwd, options.root),
|
|
62
40
|
port: Number(options.port),
|
|
63
41
|
renderOptions: {
|
|
64
|
-
seed: Number(options.seed),
|
|
65
42
|
title: options.title,
|
|
66
43
|
meta: {},
|
|
67
44
|
styles: options.styles || [],
|
|
@@ -79,40 +56,17 @@ program
|
|
|
79
56
|
.description("Build static site from pages directory")
|
|
80
57
|
.option("-r, --root <dir>", "source root directory", "src")
|
|
81
58
|
.option("-o, --out <dir>", "output directory", "dist")
|
|
82
|
-
.option("-s, --seed <seed>", "seed for random number generator", 42)
|
|
83
59
|
.option("-t, --title <title>", "default page title", "My Site")
|
|
84
60
|
.option("--styles <styles...>", "CSS files to include")
|
|
85
61
|
.option("--scripts <scripts...>", "JS files to include")
|
|
86
62
|
.action(async (options) => {
|
|
87
63
|
const cwd = process.cwd()
|
|
88
|
-
const seed = Number(options.seed)
|
|
89
64
|
|
|
90
65
|
try {
|
|
91
|
-
// 1️⃣ Install DOM *before anything else*
|
|
92
|
-
const window = new Window()
|
|
93
|
-
|
|
94
|
-
globalThis.window = window
|
|
95
|
-
globalThis.document = window.document
|
|
96
|
-
globalThis.HTMLElement = window.HTMLElement
|
|
97
|
-
globalThis.Node = window.Node
|
|
98
|
-
globalThis.Comment = window.Comment
|
|
99
|
-
|
|
100
|
-
// Optional but sometimes needed
|
|
101
|
-
globalThis.customElements = window.customElements
|
|
102
|
-
|
|
103
|
-
// 3️⃣ Patch with the parsed seed
|
|
104
|
-
const restore = patchRandom(seed)
|
|
105
|
-
await import("@inglorious/web")
|
|
106
|
-
restore()
|
|
107
|
-
|
|
108
|
-
// 4️⃣ NOW import and run build
|
|
109
|
-
const { build } = await import("../src/build.js")
|
|
110
|
-
|
|
111
66
|
await build({
|
|
112
67
|
rootDir: path.resolve(cwd, options.root),
|
|
113
68
|
outDir: path.resolve(cwd, options.out),
|
|
114
69
|
renderOptions: {
|
|
115
|
-
seed,
|
|
116
70
|
title: options.title,
|
|
117
71
|
meta: {},
|
|
118
72
|
styles: options.styles || [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ssx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Server-Side-X. Xecution? Xperience? Who knows.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@lit-labs/ssr": "^4.0.0",
|
|
44
45
|
"commander": "^14.0.2",
|
|
45
46
|
"connect": "^3.7.0",
|
|
46
47
|
"glob": "^13.0.0",
|
|
47
|
-
"happy-dom": "^20.0.11",
|
|
48
48
|
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
49
49
|
"vite": "^7.1.3",
|
|
50
|
-
"@inglorious/web": "
|
|
50
|
+
"@inglorious/web": "4.0.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"prettier": "^3.6.2",
|
package/src/build.js
CHANGED
|
@@ -7,13 +7,11 @@ import { build as viteBuild } from "vite"
|
|
|
7
7
|
import { renderPage } from "./render.js"
|
|
8
8
|
import { getPages } from "./router.js"
|
|
9
9
|
import { generateApp } from "./scripts/app.js"
|
|
10
|
-
import { generateLitLoader } from "./scripts/lit-loader.js"
|
|
11
|
-
import { generateMain } from "./scripts/main.js"
|
|
12
10
|
import { generateStore } from "./store.js"
|
|
13
11
|
import { createViteConfig } from "./vite-config.js"
|
|
14
12
|
|
|
15
13
|
export async function build(options = {}) {
|
|
16
|
-
const { rootDir = "src", outDir = "dist"
|
|
14
|
+
const { rootDir = "src", outDir = "dist" } = options
|
|
17
15
|
|
|
18
16
|
console.log("🔨 Starting build...\n")
|
|
19
17
|
|
|
@@ -25,29 +23,23 @@ export async function build(options = {}) {
|
|
|
25
23
|
const pages = await getPages(path.join(rootDir, "pages"))
|
|
26
24
|
console.log(`📄 Found ${pages.length} pages to build\n`)
|
|
27
25
|
|
|
26
|
+
// Generate store config once for all pages
|
|
27
|
+
const store = await generateStore(pages, options)
|
|
28
|
+
|
|
28
29
|
// Render all pages
|
|
29
|
-
const
|
|
30
|
+
const htmls = await renderPages(store, pages, options)
|
|
30
31
|
|
|
31
32
|
// Write all pages to disk
|
|
32
33
|
console.log("\n💾 Writing files...\n")
|
|
33
34
|
|
|
34
|
-
// Generate store config once for all pages
|
|
35
|
-
const store = await generateStore(pages, options)
|
|
36
|
-
|
|
37
|
-
// Generate lit-loader.js
|
|
38
|
-
const litLoader = generateLitLoader(renderOptions)
|
|
39
|
-
await fs.writeFile(path.join(outDir, "lit-loader.js"), litLoader, "utf-8")
|
|
40
|
-
|
|
41
35
|
const app = generateApp(store, pages)
|
|
42
|
-
await fs.writeFile(path.join(outDir, "
|
|
36
|
+
await fs.writeFile(path.join(outDir, "main.js"), app, "utf-8")
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const { page, html } of renderedPages) {
|
|
38
|
+
pages.forEach(async (page, index) => {
|
|
39
|
+
const html = htmls[index]
|
|
48
40
|
const filePath = await writePageToDisk(page.path, html, outDir)
|
|
49
41
|
console.log(` ✓ ${filePath}`)
|
|
50
|
-
}
|
|
42
|
+
})
|
|
51
43
|
|
|
52
44
|
// Bundle with Vite
|
|
53
45
|
console.log("\n📦 Bundling with Vite...\n")
|
|
@@ -55,16 +47,14 @@ export async function build(options = {}) {
|
|
|
55
47
|
await viteBuild(viteConfig)
|
|
56
48
|
|
|
57
49
|
// Remove bundled files
|
|
58
|
-
console.log("\n🧹 Cleaning up...\n")
|
|
59
|
-
await fs.rm(path.join(outDir, "lit-loader.js"))
|
|
60
|
-
await fs.rm(path.join(outDir, "app.js"))
|
|
50
|
+
// console.log("\n🧹 Cleaning up...\n")
|
|
61
51
|
|
|
62
52
|
console.log("\n✨ Build complete!\n")
|
|
63
53
|
|
|
64
|
-
return { pages:
|
|
54
|
+
return { pages: htmls.length, outDir }
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
async function
|
|
57
|
+
async function renderPages(store, pages, options = {}) {
|
|
68
58
|
const { renderOptions } = options
|
|
69
59
|
|
|
70
60
|
const renderedPages = []
|
|
@@ -72,13 +62,12 @@ async function generatePages(pages, options = {}) {
|
|
|
72
62
|
for (const page of pages) {
|
|
73
63
|
console.log(` Rendering ${page.path}...`)
|
|
74
64
|
|
|
75
|
-
const store = await generateStore([page], options)
|
|
76
65
|
const module = await import(pathToFileURL(page.filePath))
|
|
77
|
-
const html = await renderPage(store, module, {
|
|
66
|
+
const html = await renderPage(store, page, module, {
|
|
78
67
|
...renderOptions,
|
|
79
68
|
wrap: true,
|
|
80
69
|
})
|
|
81
|
-
renderedPages.push(
|
|
70
|
+
renderedPages.push(html)
|
|
82
71
|
}
|
|
83
72
|
|
|
84
73
|
return renderedPages
|
package/src/dev.js
CHANGED
|
@@ -6,8 +6,6 @@ import { createServer } from "vite"
|
|
|
6
6
|
import { renderPage } from "./render.js"
|
|
7
7
|
import { getPages } from "./router.js"
|
|
8
8
|
import { generateApp } from "./scripts/app.js"
|
|
9
|
-
import { generateLitLoader } from "./scripts/lit-loader.js"
|
|
10
|
-
import { generateMain } from "./scripts/main.js"
|
|
11
9
|
import { generateStore } from "./store.js"
|
|
12
10
|
|
|
13
11
|
export async function dev(options = {}) {
|
|
@@ -22,15 +20,6 @@ export async function dev(options = {}) {
|
|
|
22
20
|
// Generate store config once for all pages
|
|
23
21
|
const store = await generateStore(pages, options)
|
|
24
22
|
|
|
25
|
-
const litLoader = generateLitLoader(renderOptions)
|
|
26
|
-
virtualFiles.set("/lit-loader.js", litLoader)
|
|
27
|
-
|
|
28
|
-
const app = generateApp(store, pages)
|
|
29
|
-
virtualFiles.set("/app.js", app)
|
|
30
|
-
|
|
31
|
-
const main = generateMain()
|
|
32
|
-
virtualFiles.set("/main.js", main)
|
|
33
|
-
|
|
34
23
|
// Create Vite dev server
|
|
35
24
|
const viteServer = await createServer({
|
|
36
25
|
root: process.cwd(),
|
|
@@ -58,14 +47,15 @@ export async function dev(options = {}) {
|
|
|
58
47
|
const page = pages.find((p) => matchRoute(p.path, url))
|
|
59
48
|
if (!page) return next()
|
|
60
49
|
|
|
61
|
-
const store = await generateStore([page], options)
|
|
62
50
|
const module = await viteServer.ssrLoadModule(page.filePath)
|
|
63
|
-
const html = await renderPage(store, module, {
|
|
51
|
+
const html = await renderPage(store, page, module, {
|
|
64
52
|
...renderOptions,
|
|
65
53
|
wrap: true,
|
|
66
|
-
dev: true,
|
|
67
54
|
})
|
|
68
55
|
|
|
56
|
+
const app = generateApp(store, pages)
|
|
57
|
+
virtualFiles.set("/main.js", app)
|
|
58
|
+
|
|
69
59
|
res.setHeader("Content-Type", "text/html")
|
|
70
60
|
res.end(html)
|
|
71
61
|
} catch (error) {
|
package/src/html.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
import { render as ssrRender } from "@lit-labs/ssr"
|
|
3
|
+
import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
|
|
2
4
|
|
|
3
|
-
export function toHTML(store, renderFn, options = {}) {
|
|
4
|
-
const
|
|
5
|
-
|
|
5
|
+
export async function toHTML(store, renderFn, options = {}) {
|
|
6
|
+
const api = { ...store._api }
|
|
7
|
+
api.render = createRender(api)
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
+
// Generate the template
|
|
10
|
+
const template = renderFn(api)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// SSR render → HTML with hydration markers
|
|
13
|
+
const result = ssrRender(template)
|
|
14
|
+
const resultString = await collectResult(result)
|
|
12
15
|
|
|
13
|
-
const
|
|
14
|
-
? stripLitMarkers(
|
|
15
|
-
:
|
|
16
|
+
const finalHTML = options.stripLitMarkers
|
|
17
|
+
? stripLitMarkers(resultString)
|
|
18
|
+
: resultString
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return options.wrap ? wrapHTML(html, options) : html
|
|
20
|
+
return options.wrap ? wrapHTML(finalHTML, options) : finalHTML
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function stripLitMarkers(html) {
|
|
@@ -46,3 +47,29 @@ function wrapHTML(body, options) {
|
|
|
46
47
|
</body>
|
|
47
48
|
</html>`
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
function createRender(api) {
|
|
52
|
+
return function (id, options = {}) {
|
|
53
|
+
const entity = api.getEntity(id)
|
|
54
|
+
|
|
55
|
+
if (!entity) {
|
|
56
|
+
const { allowType } = options
|
|
57
|
+
if (!allowType) return ""
|
|
58
|
+
|
|
59
|
+
const type = api.getType(id)
|
|
60
|
+
if (!type?.render) {
|
|
61
|
+
console.warn(`No entity or type found: ${id}`)
|
|
62
|
+
return html`<div>Not found: ${id}</div>`
|
|
63
|
+
}
|
|
64
|
+
return type.render(api)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const type = api.getType(entity.type)
|
|
68
|
+
if (!type?.render) {
|
|
69
|
+
console.warn(`No render function for type: ${entity.type}`)
|
|
70
|
+
return html`<div>No renderer for ${entity.type}</div>`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return type.render(entity, api)
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/html.test.js
CHANGED
|
@@ -5,27 +5,27 @@ import { toHTML } from "./html.js"
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_OPTIONS = { stripLitMarkers: true }
|
|
7
7
|
|
|
8
|
-
describe("toHTML", () => {
|
|
8
|
+
describe("await toHTML", () => {
|
|
9
9
|
describe("basic rendering", () => {
|
|
10
|
-
it("should render simple HTML without wrapping", () => {
|
|
10
|
+
it("should render simple HTML without wrapping", async () => {
|
|
11
11
|
const store = createStore()
|
|
12
12
|
const renderFn = () => html`<h1>Hello World</h1>`
|
|
13
13
|
|
|
14
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
14
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
15
15
|
|
|
16
16
|
expect(result).toMatchSnapshot()
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
it("should render empty content", () => {
|
|
19
|
+
it("should render empty content", async () => {
|
|
20
20
|
const store = createStore()
|
|
21
21
|
const renderFn = () => html``
|
|
22
22
|
|
|
23
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
23
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
24
24
|
|
|
25
25
|
expect(result).toMatchSnapshot()
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
it("should render nested elements", () => {
|
|
28
|
+
it("should render nested elements", async () => {
|
|
29
29
|
const store = createStore()
|
|
30
30
|
const renderFn = () =>
|
|
31
31
|
html`<div class="container">
|
|
@@ -33,24 +33,24 @@ describe("toHTML", () => {
|
|
|
33
33
|
<p>Content</p>
|
|
34
34
|
</div>`
|
|
35
35
|
|
|
36
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
36
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
37
37
|
|
|
38
38
|
expect(result).toMatchSnapshot()
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it("should render with inline styles", () => {
|
|
41
|
+
it("should render with inline styles", async () => {
|
|
42
42
|
const store = createStore()
|
|
43
43
|
const renderFn = () =>
|
|
44
44
|
html`<div style="color: red; font-size: 16px;">Styled</div>`
|
|
45
45
|
|
|
46
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
46
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
47
47
|
|
|
48
48
|
expect(result).toMatchSnapshot()
|
|
49
49
|
})
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
describe("rendering with state", () => {
|
|
53
|
-
it("should render entities from store", () => {
|
|
53
|
+
it("should render entities from store", async () => {
|
|
54
54
|
const store = createStore({
|
|
55
55
|
types: {
|
|
56
56
|
message: {
|
|
@@ -64,12 +64,12 @@ describe("toHTML", () => {
|
|
|
64
64
|
|
|
65
65
|
const renderFn = (api) => html`<div>${api.render("greeting")}</div>`
|
|
66
66
|
|
|
67
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
67
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
68
68
|
|
|
69
69
|
expect(result).toMatchSnapshot()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it("should render multiple entities", () => {
|
|
72
|
+
it("should render multiple entities", async () => {
|
|
73
73
|
const store = createStore({
|
|
74
74
|
types: {
|
|
75
75
|
item: {
|
|
@@ -88,12 +88,12 @@ describe("toHTML", () => {
|
|
|
88
88
|
${api.render("item1")} ${api.render("item2")} ${api.render("item3")}
|
|
89
89
|
</ul>`
|
|
90
90
|
|
|
91
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
91
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
92
92
|
|
|
93
93
|
expect(result).toMatchSnapshot()
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
it("should evaluate conditional rendering based on state", () => {
|
|
96
|
+
it("should evaluate conditional rendering based on state", async () => {
|
|
97
97
|
const store = createStore({
|
|
98
98
|
types: {
|
|
99
99
|
content: {
|
|
@@ -110,18 +110,18 @@ describe("toHTML", () => {
|
|
|
110
110
|
|
|
111
111
|
const renderFn = (api) => html`<div>${api.render("content")}</div>`
|
|
112
112
|
|
|
113
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
113
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
114
114
|
|
|
115
115
|
expect(result).toMatchSnapshot()
|
|
116
116
|
})
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
describe("HTML wrapping", () => {
|
|
120
|
-
it("should wrap HTML with basic DOCTYPE and structure", () => {
|
|
120
|
+
it("should wrap HTML with basic DOCTYPE and structure", async () => {
|
|
121
121
|
const store = createStore()
|
|
122
122
|
const renderFn = () => html`<h1>Page Title</h1>`
|
|
123
123
|
|
|
124
|
-
const result = toHTML(store, renderFn, {
|
|
124
|
+
const result = await toHTML(store, renderFn, {
|
|
125
125
|
...DEFAULT_OPTIONS,
|
|
126
126
|
wrap: true,
|
|
127
127
|
title: "My Page",
|
|
@@ -130,11 +130,11 @@ describe("toHTML", () => {
|
|
|
130
130
|
expect(result).toMatchSnapshot()
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
-
it("should include meta tags in wrapped HTML", () => {
|
|
133
|
+
it("should include meta tags in wrapped HTML", async () => {
|
|
134
134
|
const store = createStore()
|
|
135
135
|
const renderFn = () => html`<p>Content</p>`
|
|
136
136
|
|
|
137
|
-
const result = toHTML(store, renderFn, {
|
|
137
|
+
const result = await toHTML(store, renderFn, {
|
|
138
138
|
...DEFAULT_OPTIONS,
|
|
139
139
|
wrap: true,
|
|
140
140
|
title: "Test Page",
|
|
@@ -147,11 +147,11 @@ describe("toHTML", () => {
|
|
|
147
147
|
expect(result).toMatchSnapshot()
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
-
it("should include stylesheets in wrapped HTML", () => {
|
|
150
|
+
it("should include stylesheets in wrapped HTML", async () => {
|
|
151
151
|
const store = createStore()
|
|
152
152
|
const renderFn = () => html`<p>Content</p>`
|
|
153
153
|
|
|
154
|
-
const result = toHTML(store, renderFn, {
|
|
154
|
+
const result = await toHTML(store, renderFn, {
|
|
155
155
|
...DEFAULT_OPTIONS,
|
|
156
156
|
wrap: true,
|
|
157
157
|
styles: ["/css/style.css", "/css/theme.css"],
|
|
@@ -160,11 +160,11 @@ describe("toHTML", () => {
|
|
|
160
160
|
expect(result).toMatchSnapshot()
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
-
it("should include scripts in wrapped HTML", () => {
|
|
163
|
+
it("should include scripts in wrapped HTML", async () => {
|
|
164
164
|
const store = createStore()
|
|
165
165
|
const renderFn = () => html`<p>Content</p>`
|
|
166
166
|
|
|
167
|
-
const result = toHTML(store, renderFn, {
|
|
167
|
+
const result = await toHTML(store, renderFn, {
|
|
168
168
|
...DEFAULT_OPTIONS,
|
|
169
169
|
wrap: true,
|
|
170
170
|
scripts: ["/js/app.js", "/js/analytics.js"],
|
|
@@ -173,11 +173,11 @@ describe("toHTML", () => {
|
|
|
173
173
|
expect(result).toMatchSnapshot()
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
it("should include all options in wrapped HTML", () => {
|
|
176
|
+
it("should include all options in wrapped HTML", async () => {
|
|
177
177
|
const store = createStore()
|
|
178
178
|
const renderFn = () => html`<main>Main content</main>`
|
|
179
179
|
|
|
180
|
-
const result = toHTML(store, renderFn, {
|
|
180
|
+
const result = await toHTML(store, renderFn, {
|
|
181
181
|
...DEFAULT_OPTIONS,
|
|
182
182
|
wrap: true,
|
|
183
183
|
title: "Complete Page",
|
|
@@ -189,20 +189,23 @@ describe("toHTML", () => {
|
|
|
189
189
|
expect(result).toMatchSnapshot()
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
-
it("should default to empty title when not provided", () => {
|
|
192
|
+
it("should default to empty title when not provided", async () => {
|
|
193
193
|
const store = createStore()
|
|
194
194
|
const renderFn = () => html`<p>Content</p>`
|
|
195
195
|
|
|
196
|
-
const result = toHTML(store, renderFn, {
|
|
196
|
+
const result = await toHTML(store, renderFn, {
|
|
197
|
+
...DEFAULT_OPTIONS,
|
|
198
|
+
wrap: true,
|
|
199
|
+
})
|
|
197
200
|
|
|
198
201
|
expect(result).toMatchSnapshot()
|
|
199
202
|
})
|
|
200
203
|
|
|
201
|
-
it("should handle empty arrays for meta, styles, and scripts", () => {
|
|
204
|
+
it("should handle empty arrays for meta, styles, and scripts", async () => {
|
|
202
205
|
const store = createStore()
|
|
203
206
|
const renderFn = () => html`<p>Content</p>`
|
|
204
207
|
|
|
205
|
-
const result = toHTML(store, renderFn, {
|
|
208
|
+
const result = await toHTML(store, renderFn, {
|
|
206
209
|
...DEFAULT_OPTIONS,
|
|
207
210
|
wrap: true,
|
|
208
211
|
meta: {},
|
|
@@ -215,7 +218,7 @@ describe("toHTML", () => {
|
|
|
215
218
|
})
|
|
216
219
|
|
|
217
220
|
describe("API rendering within components", () => {
|
|
218
|
-
it("should support api.render() method in component render function", () => {
|
|
221
|
+
it("should support api.render() method in component render function", async () => {
|
|
219
222
|
const store = createStore({
|
|
220
223
|
types: {
|
|
221
224
|
wrapper: {
|
|
@@ -231,14 +234,14 @@ describe("toHTML", () => {
|
|
|
231
234
|
|
|
232
235
|
const renderFn = (api) => html`<div>${api.render("myWrapper")}</div>`
|
|
233
236
|
|
|
234
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
237
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
235
238
|
|
|
236
239
|
expect(result).toMatchSnapshot()
|
|
237
240
|
})
|
|
238
241
|
})
|
|
239
242
|
|
|
240
243
|
describe("complex scenarios", () => {
|
|
241
|
-
it("should render a complete page structure with message list", () => {
|
|
244
|
+
it("should render a complete page structure with message list", async () => {
|
|
242
245
|
const store = createStore({
|
|
243
246
|
types: {
|
|
244
247
|
message: {
|
|
@@ -259,12 +262,12 @@ describe("toHTML", () => {
|
|
|
259
262
|
<footer>© 2024</footer>
|
|
260
263
|
</div>`
|
|
261
264
|
|
|
262
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
265
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
263
266
|
|
|
264
267
|
expect(result).toMatchSnapshot()
|
|
265
268
|
})
|
|
266
269
|
|
|
267
|
-
it("should render wrapped complex page with all assets", () => {
|
|
270
|
+
it("should render wrapped complex page with all assets", async () => {
|
|
268
271
|
const store = createStore({
|
|
269
272
|
types: {
|
|
270
273
|
header: { render: () => html`<header><h1>My Website</h1></header>` },
|
|
@@ -278,7 +281,7 @@ describe("toHTML", () => {
|
|
|
278
281
|
<p>Welcome!</p>
|
|
279
282
|
</div>`
|
|
280
283
|
|
|
281
|
-
const result = toHTML(store, renderFn, {
|
|
284
|
+
const result = await toHTML(store, renderFn, {
|
|
282
285
|
...DEFAULT_OPTIONS,
|
|
283
286
|
wrap: true,
|
|
284
287
|
title: "My Website",
|
|
@@ -295,7 +298,7 @@ describe("toHTML", () => {
|
|
|
295
298
|
})
|
|
296
299
|
|
|
297
300
|
describe("event handling", () => {
|
|
298
|
-
it("should render event handlers in templates", () => {
|
|
301
|
+
it("should render event handlers in templates", async () => {
|
|
299
302
|
const store = createStore({
|
|
300
303
|
types: {
|
|
301
304
|
button: {
|
|
@@ -312,12 +315,12 @@ describe("toHTML", () => {
|
|
|
312
315
|
|
|
313
316
|
const renderFn = (api) => html`<div>${api.render("myButton")}</div>`
|
|
314
317
|
|
|
315
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
318
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
316
319
|
|
|
317
320
|
expect(result).toMatchSnapshot()
|
|
318
321
|
})
|
|
319
322
|
|
|
320
|
-
it("should render multiple event handlers", () => {
|
|
323
|
+
it("should render multiple event handlers", async () => {
|
|
321
324
|
const store = createStore({
|
|
322
325
|
types: {
|
|
323
326
|
counter: {
|
|
@@ -340,36 +343,36 @@ describe("toHTML", () => {
|
|
|
340
343
|
|
|
341
344
|
const renderFn = (api) => html`<div>${api.render("counter1")}</div>`
|
|
342
345
|
|
|
343
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
346
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
344
347
|
|
|
345
348
|
expect(result).toMatchSnapshot()
|
|
346
349
|
})
|
|
347
350
|
})
|
|
348
351
|
|
|
349
352
|
describe("edge cases", () => {
|
|
350
|
-
it("should handle special characters in content", () => {
|
|
353
|
+
it("should handle special characters in content", async () => {
|
|
351
354
|
const store = createStore()
|
|
352
355
|
const renderFn = () => html`<p><script> & "quotes"</p>`
|
|
353
356
|
|
|
354
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
357
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
355
358
|
|
|
356
359
|
expect(result).toMatchSnapshot()
|
|
357
360
|
})
|
|
358
361
|
|
|
359
|
-
it("should not include wrap by default", () => {
|
|
362
|
+
it("should not include wrap by default", async () => {
|
|
360
363
|
const store = createStore()
|
|
361
364
|
const renderFn = () => html`<p>Content</p>`
|
|
362
365
|
|
|
363
|
-
const result = toHTML(store, renderFn, {})
|
|
366
|
+
const result = await toHTML(store, renderFn, {})
|
|
364
367
|
|
|
365
368
|
expect(result).toMatchSnapshot()
|
|
366
369
|
})
|
|
367
370
|
|
|
368
|
-
it("should return only inner HTML when wrap is false", () => {
|
|
371
|
+
it("should return only inner HTML when wrap is false", async () => {
|
|
369
372
|
const store = createStore()
|
|
370
373
|
const renderFn = () => html`<p>Inner</p>`
|
|
371
374
|
|
|
372
|
-
const result = toHTML(store, renderFn, {
|
|
375
|
+
const result = await toHTML(store, renderFn, {
|
|
373
376
|
...DEFAULT_OPTIONS,
|
|
374
377
|
wrap: false,
|
|
375
378
|
})
|
|
@@ -377,11 +380,11 @@ describe("toHTML", () => {
|
|
|
377
380
|
expect(result).toMatchSnapshot()
|
|
378
381
|
})
|
|
379
382
|
|
|
380
|
-
it("should close DOM window properly", () => {
|
|
383
|
+
it("should close DOM window properly", async () => {
|
|
381
384
|
const store = createStore()
|
|
382
385
|
const renderFn = () => html`<p>Test</p>`
|
|
383
386
|
|
|
384
|
-
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
387
|
+
const result = await toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
385
388
|
|
|
386
389
|
expect(result).toBeDefined()
|
|
387
390
|
expect(result).not.toBeNull()
|
package/src/render.js
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { toHTML } from "./html.js"
|
|
2
2
|
import { getModuleName } from "./module.js"
|
|
3
3
|
|
|
4
|
-
export async function renderPage(store,
|
|
5
|
-
const
|
|
4
|
+
export async function renderPage(store, page, module, options = {}) {
|
|
5
|
+
const { title = "", meta = {}, scripts = [], styles = [] } = options
|
|
6
|
+
|
|
7
|
+
const name = getModuleName(module)
|
|
6
8
|
const api = store._api
|
|
7
9
|
const entity = api.getEntity(name)
|
|
8
10
|
|
|
9
|
-
if (
|
|
10
|
-
await
|
|
11
|
+
if (module.load) {
|
|
12
|
+
await module.load(entity, page, store._api)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pageTitle = module.title
|
|
16
|
+
? typeof module.title === "function"
|
|
17
|
+
? module.title(entity, api)
|
|
18
|
+
: module.title
|
|
19
|
+
: title
|
|
20
|
+
|
|
21
|
+
const pageMeta = {
|
|
22
|
+
...meta,
|
|
23
|
+
...(typeof module.meta === "function"
|
|
24
|
+
? module.meta(entity, api)
|
|
25
|
+
: (module.meta ?? {})),
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
const pageScripts = [
|
|
29
|
+
...scripts,
|
|
30
|
+
...(typeof module.scripts === "function"
|
|
31
|
+
? module.scripts(entity, api)
|
|
32
|
+
: (module.scripts ?? [])),
|
|
33
|
+
]
|
|
34
|
+
const pageStyles = [
|
|
35
|
+
...styles,
|
|
36
|
+
...(typeof module.styles === "function"
|
|
37
|
+
? module.styles(entity, api)
|
|
38
|
+
: (module.styles ?? [])),
|
|
39
|
+
]
|
|
23
40
|
|
|
24
41
|
return toHTML(store, (api) => api.render(name, { allowType: true }), {
|
|
25
42
|
...options,
|
|
26
|
-
title,
|
|
27
|
-
meta,
|
|
28
|
-
scripts,
|
|
29
|
-
styles,
|
|
43
|
+
title: pageTitle,
|
|
44
|
+
meta: pageMeta,
|
|
45
|
+
scripts: pageScripts,
|
|
46
|
+
styles: pageStyles,
|
|
30
47
|
})
|
|
31
48
|
}
|
package/src/render.test.js
CHANGED
|
@@ -11,6 +11,7 @@ const PAGES_DIR = path.join(ROOT_DIR, "pages")
|
|
|
11
11
|
const DEFAULT_OPTIONS = { stripLitMarkers: true }
|
|
12
12
|
|
|
13
13
|
it("should render a static page fragment", async () => {
|
|
14
|
+
const page = { path: "/" }
|
|
14
15
|
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
15
16
|
|
|
16
17
|
const store = createStore({
|
|
@@ -18,42 +19,45 @@ it("should render a static page fragment", async () => {
|
|
|
18
19
|
updateMode: "manual",
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
const html = await renderPage(store, module, DEFAULT_OPTIONS)
|
|
22
|
+
const html = await renderPage(store, page, module, DEFAULT_OPTIONS)
|
|
22
23
|
|
|
23
24
|
expect(html).toMatchSnapshot()
|
|
24
25
|
})
|
|
25
26
|
|
|
26
|
-
it("should render a
|
|
27
|
+
it("should render a page with entity", async () => {
|
|
28
|
+
const page = { path: "/about" }
|
|
27
29
|
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
28
30
|
|
|
29
31
|
const store = createStore({
|
|
30
32
|
types: { about: module.about },
|
|
33
|
+
entities: { about: { type: "about", name: "Us" } },
|
|
31
34
|
updateMode: "manual",
|
|
32
35
|
})
|
|
33
36
|
|
|
34
|
-
const html = await renderPage(store, module,
|
|
35
|
-
...DEFAULT_OPTIONS,
|
|
36
|
-
wrap: true,
|
|
37
|
-
})
|
|
37
|
+
const html = await renderPage(store, page, module, DEFAULT_OPTIONS)
|
|
38
38
|
|
|
39
39
|
expect(html).toMatchSnapshot()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it("should render a page with
|
|
42
|
+
it("should render a page with metadata", async () => {
|
|
43
|
+
const page = { path: "/about" }
|
|
43
44
|
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
44
45
|
|
|
45
46
|
const store = createStore({
|
|
46
47
|
types: { about: module.about },
|
|
47
|
-
entities: { about: { type: "about", name: "Us" } },
|
|
48
48
|
updateMode: "manual",
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
const html = await renderPage(store, module,
|
|
51
|
+
const html = await renderPage(store, page, module, {
|
|
52
|
+
...DEFAULT_OPTIONS,
|
|
53
|
+
wrap: true,
|
|
54
|
+
})
|
|
52
55
|
|
|
53
56
|
expect(html).toMatchSnapshot()
|
|
54
57
|
})
|
|
55
58
|
|
|
56
59
|
it("should render a page with pre-fetched data", async () => {
|
|
60
|
+
const page = { path: "/posts" }
|
|
57
61
|
const module = await import(path.resolve(path.join(PAGES_DIR, "posts.js")))
|
|
58
62
|
|
|
59
63
|
const store = createStore({
|
|
@@ -62,7 +66,7 @@ it("should render a page with pre-fetched data", async () => {
|
|
|
62
66
|
updateMode: "manual",
|
|
63
67
|
})
|
|
64
68
|
|
|
65
|
-
const html = await renderPage(store, module, DEFAULT_OPTIONS)
|
|
69
|
+
const html = await renderPage(store, page, module, DEFAULT_OPTIONS)
|
|
66
70
|
|
|
67
71
|
expect(html).toMatchSnapshot()
|
|
68
72
|
})
|
package/src/router.js
CHANGED
|
@@ -3,6 +3,8 @@ import { pathToFileURL } from "node:url"
|
|
|
3
3
|
|
|
4
4
|
import { glob } from "glob"
|
|
5
5
|
|
|
6
|
+
import { getModuleName } from "./module.js"
|
|
7
|
+
|
|
6
8
|
const NEXT_MATCH = 1
|
|
7
9
|
|
|
8
10
|
const STATIC_SEGMENT_WEIGHT = 3
|
|
@@ -18,44 +20,43 @@ export async function getPages(pagesDir = "pages") {
|
|
|
18
20
|
const pages = []
|
|
19
21
|
|
|
20
22
|
for (const route of routes) {
|
|
23
|
+
const module = await import(pathToFileURL(path.resolve(route.filePath)))
|
|
24
|
+
const moduleName = getModuleName(module)
|
|
25
|
+
|
|
21
26
|
if (isDynamic(route.pattern)) {
|
|
22
27
|
// Dynamic route - call getStaticPaths if it exists
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
filePath: route.filePath,
|
|
41
|
-
params,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
console.warn(
|
|
46
|
-
`Dynamic route ${route.filePath} has no getStaticPaths export. ` +
|
|
47
|
-
`It will be skipped during SSG.`,
|
|
48
|
-
)
|
|
28
|
+
if (typeof module.getStaticPaths === "function") {
|
|
29
|
+
const paths = await module.getStaticPaths()
|
|
30
|
+
|
|
31
|
+
for (const pathOrObject of paths) {
|
|
32
|
+
const urlPath =
|
|
33
|
+
typeof pathOrObject === "string" ? pathOrObject : pathOrObject.path
|
|
34
|
+
|
|
35
|
+
const params = extractParams(route, urlPath)
|
|
36
|
+
|
|
37
|
+
pages.push({
|
|
38
|
+
pattern: route.pattern,
|
|
39
|
+
path: urlPath,
|
|
40
|
+
modulePath: route.modulePath,
|
|
41
|
+
filePath: route.filePath,
|
|
42
|
+
moduleName,
|
|
43
|
+
params,
|
|
44
|
+
})
|
|
49
45
|
}
|
|
50
|
-
}
|
|
51
|
-
console.
|
|
46
|
+
} else {
|
|
47
|
+
console.warn(
|
|
48
|
+
`Dynamic route ${route.filePath} has no getStaticPaths export. ` +
|
|
49
|
+
`It will be skipped during SSG.`,
|
|
50
|
+
)
|
|
52
51
|
}
|
|
53
52
|
} else {
|
|
54
53
|
// Static route - add directly
|
|
55
54
|
pages.push({
|
|
56
|
-
|
|
55
|
+
pattern: route.pattern,
|
|
56
|
+
path: route.pattern || "/",
|
|
57
57
|
modulePath: route.modulePath,
|
|
58
58
|
filePath: route.filePath,
|
|
59
|
+
moduleName,
|
|
59
60
|
params: {},
|
|
60
61
|
})
|
|
61
62
|
}
|
package/src/scripts/app.js
CHANGED
|
@@ -1,41 +1,58 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generate the code that goes inside the <!-- SSX --> marker.
|
|
3
5
|
* This creates the types and entities objects for the client-side store.
|
|
4
6
|
*/
|
|
5
7
|
export function generateApp(store, pages) {
|
|
6
8
|
// Collect all unique page modules and their exports
|
|
7
|
-
const
|
|
8
|
-
(page) =>
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const routes = pages
|
|
10
|
+
.filter((page, index) => {
|
|
11
|
+
return pages.findIndex((p) => p.pattern === page.pattern) === index
|
|
12
|
+
})
|
|
13
|
+
.map(
|
|
14
|
+
(page) =>
|
|
15
|
+
` "${page.pattern}": () => import("@/pages/${page.modulePath}")`,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
return `import { createDevtools, createStore, mount } from "@inglorious/web"
|
|
13
|
-
import { router } from "@inglorious/web/router"
|
|
19
|
+
import { getRoute, router, setRoutes } from "@inglorious/web/router"
|
|
20
|
+
|
|
21
|
+
const pages = ${JSON.stringify(pages.map(({ filePath, ...page }) => page))}
|
|
22
|
+
const path = window.location.pathname + window.location.search + window.location.hash
|
|
23
|
+
const page = pages.find((page) => page.path === path)
|
|
14
24
|
|
|
15
25
|
const types = { router }
|
|
16
26
|
|
|
17
27
|
const entities = {
|
|
18
28
|
router: {
|
|
19
29
|
type: "router",
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
30
|
+
path: page.path,
|
|
31
|
+
route: page.moduleName,
|
|
23
32
|
},
|
|
24
33
|
${JSON.stringify(store.getState(), null, 2).slice(1, -1)}
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
const middlewares = []
|
|
28
|
-
|
|
37
|
+
if (import.meta.env.DEV) {
|
|
29
38
|
middlewares.push(createDevtools().middleware)
|
|
30
|
-
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setRoutes({
|
|
42
|
+
${routes.join(",\n")}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const module = await getRoute(page.pattern)()
|
|
46
|
+
const type = module[page.moduleName]
|
|
47
|
+
types[page.moduleName] = type
|
|
31
48
|
|
|
32
49
|
const store = createStore({ types, entities, middlewares })
|
|
33
50
|
|
|
34
51
|
const root = document.getElementById("root")
|
|
35
|
-
root.innerHTML = ""
|
|
36
52
|
|
|
37
53
|
mount(store, (api) => {
|
|
38
54
|
const { route } = api.getEntity("router")
|
|
39
55
|
return api.render(route, { allowType: true })
|
|
40
|
-
}, root)
|
|
56
|
+
}, root)
|
|
57
|
+
`
|
|
41
58
|
}
|
package/src/vite-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
|
|
3
|
-
import { minifyTemplateLiterals } from "rollup-plugin-minify-template-literals"
|
|
3
|
+
// import { minifyTemplateLiterals } from "rollup-plugin-minify-template-literals"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate Vite config for building the client bundle
|
|
@@ -9,8 +9,8 @@ export function createViteConfig(options = {}) {
|
|
|
9
9
|
const { rootDir = "src", outDir = "dist" } = options
|
|
10
10
|
|
|
11
11
|
return {
|
|
12
|
-
root:
|
|
13
|
-
plugins: [minifyTemplateLiterals()],
|
|
12
|
+
root: rootDir,
|
|
13
|
+
// plugins: [minifyTemplateLiterals()], // TODO: minification breaks hydration. The footprint difference is minimal after all
|
|
14
14
|
build: {
|
|
15
15
|
outDir,
|
|
16
16
|
emptyOutDir: false, // Don't delete HTML files we already generated
|
|
@@ -22,6 +22,12 @@ export function createViteConfig(options = {}) {
|
|
|
22
22
|
entryFileNames: "[name].js",
|
|
23
23
|
chunkFileNames: "[name].[hash].js",
|
|
24
24
|
assetFileNames: "[name].[ext]",
|
|
25
|
+
|
|
26
|
+
manualChunks(id) {
|
|
27
|
+
if (id.includes("node_modules")) {
|
|
28
|
+
return "lib"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
25
31
|
},
|
|
26
32
|
},
|
|
27
33
|
},
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export function generateLitLoader(options = {}) {
|
|
2
|
-
return `let seed = ${options.seed}
|
|
3
|
-
let mode = "seeded"
|
|
4
|
-
|
|
5
|
-
const originalRandom = Math.random
|
|
6
|
-
Math.random = random
|
|
7
|
-
|
|
8
|
-
await import("@inglorious/web")
|
|
9
|
-
|
|
10
|
-
queueMicrotask(() => {
|
|
11
|
-
Math.random = originalRandom
|
|
12
|
-
mode = "normal"
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
function random() {
|
|
16
|
-
if (mode === "seeded") {
|
|
17
|
-
seed = (seed * 1664525 + 1013904223) % 4294967296
|
|
18
|
-
return seed / 4294967296
|
|
19
|
-
}
|
|
20
|
-
return originalRandom()
|
|
21
|
-
}
|
|
22
|
-
`
|
|
23
|
-
}
|