@inglorious/ssx 0.1.4 → 0.2.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/bin/ssx.js +77 -0
- package/package.json +18 -11
- package/src/build.js +125 -0
- package/src/build.test.js +11 -0
- package/src/html.js +15 -6
- package/src/html.test.js +42 -27
- package/src/module.js +20 -0
- package/src/random.js +30 -0
- package/src/render.js +31 -0
- package/src/render.test.js +67 -0
- package/src/router.js +230 -0
- package/src/router.test.js +103 -0
- package/src/scripts/app.js +70 -0
- package/src/scripts/lit-loader.js +23 -0
- package/src/scripts/main.js +5 -0
- package/src/vite-config.js +38 -0
- package/src/__snapshots__/html.test.js.snap +0 -193
package/bin/ssx.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
import { Command } from "commander"
|
|
7
|
+
import { Window } from "happy-dom"
|
|
8
|
+
|
|
9
|
+
import { patchRandom } from "../src/random.js"
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = path.dirname(__filename)
|
|
13
|
+
|
|
14
|
+
// Read package.json for version
|
|
15
|
+
const packageJson = JSON.parse(
|
|
16
|
+
await readFile(path.join(__dirname, "../package.json"), "utf-8"),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const program = new Command()
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name("ssx")
|
|
23
|
+
.description("Static Site Xecution for @inglorious/web")
|
|
24
|
+
.version(packageJson.version)
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command("build")
|
|
28
|
+
.description("Build static site from pages directory")
|
|
29
|
+
.option("-r, --root <dir>", "source root directory", "src")
|
|
30
|
+
.option("-o, --out <dir>", "output directory", "dist")
|
|
31
|
+
.option("-s, --seed <seed>", "seed for random number generator", 42)
|
|
32
|
+
.option("-t, --title <title>", "default page title", "My Site")
|
|
33
|
+
.option("--styles <styles...>", "CSS files to include")
|
|
34
|
+
.option("--scripts <scripts...>", "JS files to include")
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
const cwd = process.cwd()
|
|
37
|
+
const seed = Number(options.seed)
|
|
38
|
+
|
|
39
|
+
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 build
|
|
58
|
+
const { build } = await import("../src/build.js")
|
|
59
|
+
|
|
60
|
+
await build({
|
|
61
|
+
rootDir: path.resolve(cwd, options.root),
|
|
62
|
+
outDir: path.resolve(cwd, options.out),
|
|
63
|
+
renderOptions: {
|
|
64
|
+
seed,
|
|
65
|
+
title: options.title,
|
|
66
|
+
meta: {},
|
|
67
|
+
styles: options.styles || [],
|
|
68
|
+
scripts: options.scripts || [],
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Build failed:", error)
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
program.parse()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ssx",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -22,28 +22,33 @@
|
|
|
22
22
|
"framework"
|
|
23
23
|
],
|
|
24
24
|
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"ssx": "./bin/ssx.js"
|
|
27
|
+
},
|
|
25
28
|
"exports": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"./*": {
|
|
31
|
-
"types": "./types/*.d.ts",
|
|
32
|
-
"import": "./src/*"
|
|
33
|
-
}
|
|
29
|
+
"./build": "./src/build.js",
|
|
30
|
+
"./router": "./src/router.js",
|
|
31
|
+
"./render": "./src/render.js",
|
|
32
|
+
"./html": "./src/html.js"
|
|
34
33
|
},
|
|
35
34
|
"files": [
|
|
36
|
-
"
|
|
35
|
+
"bin",
|
|
36
|
+
"src",
|
|
37
|
+
"!src/__fixtures__",
|
|
38
|
+
"!src/__snapshots__"
|
|
37
39
|
],
|
|
38
40
|
"publishConfig": {
|
|
39
41
|
"access": "public"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
44
|
+
"commander": "^14.0.2",
|
|
45
|
+
"glob": "^13.0.0",
|
|
42
46
|
"happy-dom": "^20.0.11",
|
|
43
47
|
"@inglorious/web": "2.6.1"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"prettier": "^3.6.2",
|
|
51
|
+
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
47
52
|
"vite": "^7.1.3",
|
|
48
53
|
"vitest": "^1.6.1",
|
|
49
54
|
"@inglorious/eslint-config": "1.1.1"
|
|
@@ -55,6 +60,8 @@
|
|
|
55
60
|
"format": "prettier --write '**/*.{js,jsx}'",
|
|
56
61
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
57
62
|
"test:watch": "vitest",
|
|
58
|
-
"test": "vitest run"
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"dev": "node ./bin/ssx.js build -r ./src/__fixtures__",
|
|
65
|
+
"serve": "pnpm dlx serve dist"
|
|
59
66
|
}
|
|
60
67
|
}
|
package/src/build.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
|
|
5
|
+
import { createStore } from "@inglorious/web"
|
|
6
|
+
import { build as viteBuild } from "vite"
|
|
7
|
+
|
|
8
|
+
import { getModuleName } from "./module.js"
|
|
9
|
+
import { renderPage } from "./render.js"
|
|
10
|
+
import { getPages } from "./router.js"
|
|
11
|
+
import { generateApp } from "./scripts/app.js"
|
|
12
|
+
import { generateLitLoader } from "./scripts/lit-loader.js"
|
|
13
|
+
import { generateMain } from "./scripts/main.js"
|
|
14
|
+
import { createViteConfig } from "./vite-config.js"
|
|
15
|
+
|
|
16
|
+
export async function build(options = {}) {
|
|
17
|
+
const { rootDir = "src", outDir = "dist", renderOptions = {} } = options
|
|
18
|
+
|
|
19
|
+
console.log("🔨 Starting build...\n")
|
|
20
|
+
|
|
21
|
+
// Clean output directory
|
|
22
|
+
await fs.rm(outDir, { recursive: true, force: true })
|
|
23
|
+
await fs.mkdir(outDir, { recursive: true })
|
|
24
|
+
|
|
25
|
+
// Get all pages to build
|
|
26
|
+
const pages = await getPages(path.join(rootDir, "pages"))
|
|
27
|
+
console.log(`📄 Found ${pages.length} pages to build\n`)
|
|
28
|
+
|
|
29
|
+
// Render all pages
|
|
30
|
+
const renderedPages = await generatePages(pages, options)
|
|
31
|
+
|
|
32
|
+
// Write all pages to disk
|
|
33
|
+
console.log("\n💾 Writing files...\n")
|
|
34
|
+
|
|
35
|
+
// Generate store config once for all pages
|
|
36
|
+
const store = await generateStore(pages, options)
|
|
37
|
+
|
|
38
|
+
// Generate lit-loader.js
|
|
39
|
+
const litLoader = generateLitLoader(renderOptions)
|
|
40
|
+
await fs.writeFile(path.join(outDir, "lit-loader.js"), litLoader, "utf-8")
|
|
41
|
+
|
|
42
|
+
const app = generateApp(store, renderedPages)
|
|
43
|
+
await fs.writeFile(path.join(outDir, "app.js"), app, "utf-8")
|
|
44
|
+
|
|
45
|
+
const main = generateMain()
|
|
46
|
+
await fs.writeFile(path.join(outDir, "main.js"), main, "utf-8")
|
|
47
|
+
|
|
48
|
+
for (const { page, html } of renderedPages) {
|
|
49
|
+
const filePath = await writePageToDisk(page.path, html, outDir)
|
|
50
|
+
console.log(` ✓ ${filePath}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bundle with Vite
|
|
54
|
+
console.log("\n📦 Bundling with Vite...\n")
|
|
55
|
+
const viteConfig = createViteConfig({ rootDir, outDir })
|
|
56
|
+
await viteBuild(viteConfig)
|
|
57
|
+
|
|
58
|
+
// Remove bundled files
|
|
59
|
+
console.log("\n🧹 Cleaning up...\n")
|
|
60
|
+
await fs.rm(path.join(outDir, "lit-loader.js"))
|
|
61
|
+
await fs.rm(path.join(outDir, "app.js"))
|
|
62
|
+
|
|
63
|
+
console.log("\n✨ Build complete!\n")
|
|
64
|
+
|
|
65
|
+
return { pages: renderedPages.length, outDir }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function generateStore(pages = [], options = {}) {
|
|
69
|
+
const { rootDir = "src" } = options
|
|
70
|
+
|
|
71
|
+
const types = {}
|
|
72
|
+
for (const page of pages) {
|
|
73
|
+
const pageModule = await import(pathToFileURL(page.filePath))
|
|
74
|
+
const name = getModuleName(pageModule)
|
|
75
|
+
types[name] = pageModule[name]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { entities } = await import(
|
|
79
|
+
pathToFileURL(path.join(rootDir, "entities.js"))
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return createStore({ types, entities, updateMode: "manual" })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function generatePages(pages, options = {}) {
|
|
86
|
+
const { renderOptions } = options
|
|
87
|
+
|
|
88
|
+
const renderedPages = []
|
|
89
|
+
|
|
90
|
+
for (const page of pages) {
|
|
91
|
+
console.log(` Rendering ${page.path}...`)
|
|
92
|
+
|
|
93
|
+
const store = await generateStore([page], options)
|
|
94
|
+
const module = await import(pathToFileURL(page.filePath))
|
|
95
|
+
const html = await renderPage(store, module, {
|
|
96
|
+
...renderOptions,
|
|
97
|
+
wrap: true,
|
|
98
|
+
})
|
|
99
|
+
renderedPages.push({ page, module, html })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return renderedPages
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write a page to disk with proper directory structure.
|
|
107
|
+
*/
|
|
108
|
+
async function writePageToDisk(pagePath, html, outDir = "dist") {
|
|
109
|
+
// Convert URL path to file path
|
|
110
|
+
// / -> dist/index.html
|
|
111
|
+
// /about -> dist/about/index.html
|
|
112
|
+
// /blog/post-1 -> dist/blog/post-1/index.html
|
|
113
|
+
|
|
114
|
+
// Remove leading slash
|
|
115
|
+
const cleanPath = pagePath.replace(/^\//, "")
|
|
116
|
+
const filePath = path.join(outDir, cleanPath, "index.html")
|
|
117
|
+
|
|
118
|
+
// Ensure directory exists
|
|
119
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
120
|
+
|
|
121
|
+
// Write file
|
|
122
|
+
await fs.writeFile(filePath, html, "utf-8")
|
|
123
|
+
|
|
124
|
+
return filePath
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { it } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { build } from "./build"
|
|
6
|
+
|
|
7
|
+
const FIXTURES_DIR = path.join(__dirname, "__fixtures__")
|
|
8
|
+
|
|
9
|
+
it.skip("should build full static pages", async () => {
|
|
10
|
+
await build({ rootDir: FIXTURES_DIR })
|
|
11
|
+
})
|
package/src/html.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { mount } from "@inglorious/web"
|
|
2
|
-
import { Window } from "happy-dom"
|
|
3
2
|
|
|
4
3
|
export function toHTML(store, renderFn, options = {}) {
|
|
5
|
-
const window =
|
|
4
|
+
const window = globalThis.window
|
|
6
5
|
const document = window.document
|
|
7
|
-
document.body.innerHTML = '<div id="root"></div>'
|
|
8
6
|
|
|
7
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
9
8
|
const root = document.getElementById("root")
|
|
9
|
+
|
|
10
10
|
mount(store, renderFn, root)
|
|
11
|
+
store.update()
|
|
12
|
+
|
|
13
|
+
const html = options.stripLitMarkers
|
|
14
|
+
? stripLitMarkers(root.innerHTML)
|
|
15
|
+
: root.innerHTML
|
|
11
16
|
|
|
12
|
-
const html = stripLitMarkers(root.innerHTML)
|
|
13
17
|
window.close()
|
|
14
18
|
|
|
15
19
|
return options.wrap ? wrapHTML(html, options) : html
|
|
@@ -22,17 +26,22 @@ function stripLitMarkers(html) {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function wrapHTML(body, options) {
|
|
25
|
-
const { title = "",
|
|
29
|
+
const { title = "", meta = {}, styles = [], scripts = [] } = options
|
|
30
|
+
|
|
26
31
|
return `<!DOCTYPE html>
|
|
27
32
|
<html>
|
|
28
33
|
<head>
|
|
29
34
|
<meta charset="UTF-8">
|
|
30
35
|
<title>${title}</title>
|
|
31
|
-
${
|
|
36
|
+
${Object.entries(meta)
|
|
37
|
+
.map(([name, content]) => `<meta name="${name}" content="${content}">`)
|
|
38
|
+
.join("\n")}
|
|
32
39
|
${styles.map((href) => `<link rel="stylesheet" href="${href}">`).join("\n")}
|
|
33
40
|
</head>
|
|
34
41
|
<body>
|
|
35
42
|
<div id="root">${body}</div>
|
|
43
|
+
|
|
44
|
+
<script type="module" src="/main.js"></script>
|
|
36
45
|
${scripts.map((src) => `<script type="module" src="${src}"></script>`).join("\n")}
|
|
37
46
|
</body>
|
|
38
47
|
</html>`
|
package/src/html.test.js
CHANGED
|
@@ -3,13 +3,15 @@ import { describe, expect, it } from "vitest"
|
|
|
3
3
|
|
|
4
4
|
import { toHTML } from "./html.js"
|
|
5
5
|
|
|
6
|
+
const DEFAULT_OPTIONS = { stripLitMarkers: true }
|
|
7
|
+
|
|
6
8
|
describe("toHTML", () => {
|
|
7
9
|
describe("basic rendering", () => {
|
|
8
10
|
it("should render simple HTML without wrapping", () => {
|
|
9
11
|
const store = createStore()
|
|
10
12
|
const renderFn = () => html`<h1>Hello World</h1>`
|
|
11
13
|
|
|
12
|
-
const result = toHTML(store, renderFn)
|
|
14
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
13
15
|
|
|
14
16
|
expect(result).toMatchSnapshot()
|
|
15
17
|
})
|
|
@@ -18,7 +20,7 @@ describe("toHTML", () => {
|
|
|
18
20
|
const store = createStore()
|
|
19
21
|
const renderFn = () => html``
|
|
20
22
|
|
|
21
|
-
const result = toHTML(store, renderFn)
|
|
23
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
22
24
|
|
|
23
25
|
expect(result).toMatchSnapshot()
|
|
24
26
|
})
|
|
@@ -31,7 +33,7 @@ describe("toHTML", () => {
|
|
|
31
33
|
<p>Content</p>
|
|
32
34
|
</div>`
|
|
33
35
|
|
|
34
|
-
const result = toHTML(store, renderFn)
|
|
36
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
35
37
|
|
|
36
38
|
expect(result).toMatchSnapshot()
|
|
37
39
|
})
|
|
@@ -41,7 +43,7 @@ describe("toHTML", () => {
|
|
|
41
43
|
const renderFn = () =>
|
|
42
44
|
html`<div style="color: red; font-size: 16px;">Styled</div>`
|
|
43
45
|
|
|
44
|
-
const result = toHTML(store, renderFn)
|
|
46
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
45
47
|
|
|
46
48
|
expect(result).toMatchSnapshot()
|
|
47
49
|
})
|
|
@@ -62,7 +64,7 @@ describe("toHTML", () => {
|
|
|
62
64
|
|
|
63
65
|
const renderFn = (api) => html`<div>${api.render("greeting")}</div>`
|
|
64
66
|
|
|
65
|
-
const result = toHTML(store, renderFn)
|
|
67
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
66
68
|
|
|
67
69
|
expect(result).toMatchSnapshot()
|
|
68
70
|
})
|
|
@@ -86,7 +88,7 @@ describe("toHTML", () => {
|
|
|
86
88
|
${api.render("item1")} ${api.render("item2")} ${api.render("item3")}
|
|
87
89
|
</ul>`
|
|
88
90
|
|
|
89
|
-
const result = toHTML(store, renderFn)
|
|
91
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
90
92
|
|
|
91
93
|
expect(result).toMatchSnapshot()
|
|
92
94
|
})
|
|
@@ -108,7 +110,7 @@ describe("toHTML", () => {
|
|
|
108
110
|
|
|
109
111
|
const renderFn = (api) => html`<div>${api.render("content")}</div>`
|
|
110
112
|
|
|
111
|
-
const result = toHTML(store, renderFn)
|
|
113
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
112
114
|
|
|
113
115
|
expect(result).toMatchSnapshot()
|
|
114
116
|
})
|
|
@@ -119,7 +121,11 @@ describe("toHTML", () => {
|
|
|
119
121
|
const store = createStore()
|
|
120
122
|
const renderFn = () => html`<h1>Page Title</h1>`
|
|
121
123
|
|
|
122
|
-
const result = toHTML(store, renderFn, {
|
|
124
|
+
const result = toHTML(store, renderFn, {
|
|
125
|
+
...DEFAULT_OPTIONS,
|
|
126
|
+
wrap: true,
|
|
127
|
+
title: "My Page",
|
|
128
|
+
})
|
|
123
129
|
|
|
124
130
|
expect(result).toMatchSnapshot()
|
|
125
131
|
})
|
|
@@ -129,12 +135,13 @@ describe("toHTML", () => {
|
|
|
129
135
|
const renderFn = () => html`<p>Content</p>`
|
|
130
136
|
|
|
131
137
|
const result = toHTML(store, renderFn, {
|
|
138
|
+
...DEFAULT_OPTIONS,
|
|
132
139
|
wrap: true,
|
|
133
140
|
title: "Test Page",
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
meta: {
|
|
142
|
+
description: "Test description",
|
|
143
|
+
viewport: "width=device-width, initial-scale=1",
|
|
144
|
+
},
|
|
138
145
|
})
|
|
139
146
|
|
|
140
147
|
expect(result).toMatchSnapshot()
|
|
@@ -145,6 +152,7 @@ describe("toHTML", () => {
|
|
|
145
152
|
const renderFn = () => html`<p>Content</p>`
|
|
146
153
|
|
|
147
154
|
const result = toHTML(store, renderFn, {
|
|
155
|
+
...DEFAULT_OPTIONS,
|
|
148
156
|
wrap: true,
|
|
149
157
|
styles: ["/css/style.css", "/css/theme.css"],
|
|
150
158
|
})
|
|
@@ -157,6 +165,7 @@ describe("toHTML", () => {
|
|
|
157
165
|
const renderFn = () => html`<p>Content</p>`
|
|
158
166
|
|
|
159
167
|
const result = toHTML(store, renderFn, {
|
|
168
|
+
...DEFAULT_OPTIONS,
|
|
160
169
|
wrap: true,
|
|
161
170
|
scripts: ["/js/app.js", "/js/analytics.js"],
|
|
162
171
|
})
|
|
@@ -169,9 +178,10 @@ describe("toHTML", () => {
|
|
|
169
178
|
const renderFn = () => html`<main>Main content</main>`
|
|
170
179
|
|
|
171
180
|
const result = toHTML(store, renderFn, {
|
|
181
|
+
...DEFAULT_OPTIONS,
|
|
172
182
|
wrap: true,
|
|
173
183
|
title: "Complete Page",
|
|
174
|
-
|
|
184
|
+
meta: { author: "Test Author" },
|
|
175
185
|
styles: ["/style.css"],
|
|
176
186
|
scripts: ["/app.js"],
|
|
177
187
|
})
|
|
@@ -183,18 +193,19 @@ describe("toHTML", () => {
|
|
|
183
193
|
const store = createStore()
|
|
184
194
|
const renderFn = () => html`<p>Content</p>`
|
|
185
195
|
|
|
186
|
-
const result = toHTML(store, renderFn, { wrap: true })
|
|
196
|
+
const result = toHTML(store, renderFn, { ...DEFAULT_OPTIONS, wrap: true })
|
|
187
197
|
|
|
188
198
|
expect(result).toMatchSnapshot()
|
|
189
199
|
})
|
|
190
200
|
|
|
191
|
-
it("should handle empty arrays for
|
|
201
|
+
it("should handle empty arrays for meta, styles, and scripts", () => {
|
|
192
202
|
const store = createStore()
|
|
193
203
|
const renderFn = () => html`<p>Content</p>`
|
|
194
204
|
|
|
195
205
|
const result = toHTML(store, renderFn, {
|
|
206
|
+
...DEFAULT_OPTIONS,
|
|
196
207
|
wrap: true,
|
|
197
|
-
|
|
208
|
+
meta: {},
|
|
198
209
|
styles: [],
|
|
199
210
|
scripts: [],
|
|
200
211
|
})
|
|
@@ -220,7 +231,7 @@ describe("toHTML", () => {
|
|
|
220
231
|
|
|
221
232
|
const renderFn = (api) => html`<div>${api.render("myWrapper")}</div>`
|
|
222
233
|
|
|
223
|
-
const result = toHTML(store, renderFn)
|
|
234
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
224
235
|
|
|
225
236
|
expect(result).toMatchSnapshot()
|
|
226
237
|
})
|
|
@@ -248,7 +259,7 @@ describe("toHTML", () => {
|
|
|
248
259
|
<footer>© 2024</footer>
|
|
249
260
|
</div>`
|
|
250
261
|
|
|
251
|
-
const result = toHTML(store, renderFn)
|
|
262
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
252
263
|
|
|
253
264
|
expect(result).toMatchSnapshot()
|
|
254
265
|
})
|
|
@@ -268,12 +279,13 @@ describe("toHTML", () => {
|
|
|
268
279
|
</div>`
|
|
269
280
|
|
|
270
281
|
const result = toHTML(store, renderFn, {
|
|
282
|
+
...DEFAULT_OPTIONS,
|
|
271
283
|
wrap: true,
|
|
272
284
|
title: "My Website",
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
meta: {
|
|
286
|
+
description: "Welcome to my website",
|
|
287
|
+
viewport: "width=device-width",
|
|
288
|
+
},
|
|
277
289
|
styles: ["/style.css"],
|
|
278
290
|
scripts: ["/script.js"],
|
|
279
291
|
})
|
|
@@ -300,7 +312,7 @@ describe("toHTML", () => {
|
|
|
300
312
|
|
|
301
313
|
const renderFn = (api) => html`<div>${api.render("myButton")}</div>`
|
|
302
314
|
|
|
303
|
-
const result = toHTML(store, renderFn)
|
|
315
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
304
316
|
|
|
305
317
|
expect(result).toMatchSnapshot()
|
|
306
318
|
})
|
|
@@ -328,7 +340,7 @@ describe("toHTML", () => {
|
|
|
328
340
|
|
|
329
341
|
const renderFn = (api) => html`<div>${api.render("counter1")}</div>`
|
|
330
342
|
|
|
331
|
-
const result = toHTML(store, renderFn)
|
|
343
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
332
344
|
|
|
333
345
|
expect(result).toMatchSnapshot()
|
|
334
346
|
})
|
|
@@ -339,7 +351,7 @@ describe("toHTML", () => {
|
|
|
339
351
|
const store = createStore()
|
|
340
352
|
const renderFn = () => html`<p><script> & "quotes"</p>`
|
|
341
353
|
|
|
342
|
-
const result = toHTML(store, renderFn)
|
|
354
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
343
355
|
|
|
344
356
|
expect(result).toMatchSnapshot()
|
|
345
357
|
})
|
|
@@ -357,7 +369,10 @@ describe("toHTML", () => {
|
|
|
357
369
|
const store = createStore()
|
|
358
370
|
const renderFn = () => html`<p>Inner</p>`
|
|
359
371
|
|
|
360
|
-
const result = toHTML(store, renderFn, {
|
|
372
|
+
const result = toHTML(store, renderFn, {
|
|
373
|
+
...DEFAULT_OPTIONS,
|
|
374
|
+
wrap: false,
|
|
375
|
+
})
|
|
361
376
|
|
|
362
377
|
expect(result).toMatchSnapshot()
|
|
363
378
|
})
|
|
@@ -366,7 +381,7 @@ describe("toHTML", () => {
|
|
|
366
381
|
const store = createStore()
|
|
367
382
|
const renderFn = () => html`<p>Test</p>`
|
|
368
383
|
|
|
369
|
-
const result = toHTML(store, renderFn)
|
|
384
|
+
const result = toHTML(store, renderFn, DEFAULT_OPTIONS)
|
|
370
385
|
|
|
371
386
|
expect(result).toBeDefined()
|
|
372
387
|
expect(result).not.toBeNull()
|
package/src/module.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const RESERVED = ["meta", "getStaticPaths", "getData"]
|
|
2
|
+
|
|
3
|
+
export function getModuleName(pageModule) {
|
|
4
|
+
const name = Object.keys(pageModule).find((key) => {
|
|
5
|
+
if (RESERVED.includes(key)) return false
|
|
6
|
+
const value = pageModule[key]
|
|
7
|
+
return (
|
|
8
|
+
value && typeof value === "object" && typeof value.render === "function"
|
|
9
|
+
)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
if (!name) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Page module must export an entity with a render() method. " +
|
|
15
|
+
`Found exports: ${Object.keys(pageModule).join(", ")}`,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return name
|
|
20
|
+
}
|
package/src/random.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
let mode = "normal"
|
|
2
|
+
let seed = 0
|
|
3
|
+
|
|
4
|
+
export function patchRandom(seed) {
|
|
5
|
+
const original = Math.random
|
|
6
|
+
const restore = setSeed(seed)
|
|
7
|
+
|
|
8
|
+
Math.random = random
|
|
9
|
+
|
|
10
|
+
return () => {
|
|
11
|
+
restore()
|
|
12
|
+
Math.random = original
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function random() {
|
|
17
|
+
if (mode === "seeded") {
|
|
18
|
+
seed = (seed * 1664525 + 1013904223) % 4294967296
|
|
19
|
+
return seed / 4294967296
|
|
20
|
+
}
|
|
21
|
+
return Math.random()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setSeed(newSeed) {
|
|
25
|
+
seed = newSeed
|
|
26
|
+
mode = "seeded"
|
|
27
|
+
return () => {
|
|
28
|
+
mode = "normal"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/render.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { toHTML } from "./html.js"
|
|
2
|
+
import { getModuleName } from "./module.js"
|
|
3
|
+
|
|
4
|
+
export async function renderPage(store, pageModule, options) {
|
|
5
|
+
const name = getModuleName(pageModule)
|
|
6
|
+
const api = store._api
|
|
7
|
+
const entity = api.getEntity(name)
|
|
8
|
+
|
|
9
|
+
if (pageModule.load) {
|
|
10
|
+
await pageModule.load(entity, store._api)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const title =
|
|
14
|
+
typeof pageModule.title === "function"
|
|
15
|
+
? pageModule.title(entity, api)
|
|
16
|
+
: pageModule.title
|
|
17
|
+
const meta =
|
|
18
|
+
typeof options.meta === "function"
|
|
19
|
+
? options.meta(entity, api)
|
|
20
|
+
: options.meta
|
|
21
|
+
const scripts = pageModule.scripts
|
|
22
|
+
const styles = pageModule.styles
|
|
23
|
+
|
|
24
|
+
return toHTML(store, (api) => api.render(name, { allowType: true }), {
|
|
25
|
+
...options,
|
|
26
|
+
title,
|
|
27
|
+
meta,
|
|
28
|
+
scripts,
|
|
29
|
+
styles,
|
|
30
|
+
})
|
|
31
|
+
}
|