@inglorious/ssx 0.2.0 → 0.2.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/bin/ssx.js +77 -0
- package/package.json +18 -12
- package/src/build.js +124 -12
- 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 +26 -12
- package/src/render.test.js +67 -0
- package/src/router.js +9 -4
- package/src/router.test.js +2 -16
- 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/__fixtures__/pages/about.js +0 -7
- package/src/__fixtures__/pages/api/[...path].js +0 -7
- package/src/__fixtures__/pages/blog/[slug].js +0 -7
- package/src/__fixtures__/pages/index.js +0 -7
- package/src/__fixtures__/pages/posts/[id].js +0 -11
- 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.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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,29 +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",
|
|
42
45
|
"glob": "^13.0.0",
|
|
43
46
|
"happy-dom": "^20.0.11",
|
|
44
|
-
"@inglorious/web": "
|
|
47
|
+
"@inglorious/web": "3.0.0"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"prettier": "^3.6.2",
|
|
51
|
+
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
48
52
|
"vite": "^7.1.3",
|
|
49
53
|
"vitest": "^1.6.1",
|
|
50
54
|
"@inglorious/eslint-config": "1.1.1"
|
|
@@ -56,6 +60,8 @@
|
|
|
56
60
|
"format": "prettier --write '**/*.{js,jsx}'",
|
|
57
61
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
58
62
|
"test:watch": "vitest",
|
|
59
|
-
"test": "vitest run"
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"dev": "node ./bin/ssx.js build -r ./src/__fixtures__",
|
|
65
|
+
"preview": "pnpm dlx serve -s dist"
|
|
60
66
|
}
|
|
61
67
|
}
|
package/src/build.js
CHANGED
|
@@ -1,13 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
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 = ["title", "meta", "scripts", "styles", "load"]
|
|
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
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toHTML } from "./html.js"
|
|
2
|
+
import { getModuleName } from "./module.js"
|
|
2
3
|
|
|
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)
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const store = createStore(storeConfig)
|
|
9
|
+
if (pageModule.load) {
|
|
10
|
+
await pageModule.load(entity, store._api)
|
|
11
|
+
}
|
|
9
12
|
|
|
10
|
-
const
|
|
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
|
|
11
23
|
|
|
12
|
-
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
return toHTML(store, (api) => api.render(name, { allowType: true }), {
|
|
25
|
+
...options,
|
|
26
|
+
title,
|
|
27
|
+
meta,
|
|
28
|
+
scripts,
|
|
29
|
+
styles,
|
|
30
|
+
})
|
|
17
31
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { createStore } from "@inglorious/web"
|
|
4
|
+
import { expect, it } from "vitest"
|
|
5
|
+
|
|
6
|
+
import { renderPage } from "./render"
|
|
7
|
+
|
|
8
|
+
const PAGES_DIR = path.join(__dirname, "__fixtures__", "pages")
|
|
9
|
+
|
|
10
|
+
const DEFAULT_OPTIONS = { stripLitMarkers: true }
|
|
11
|
+
|
|
12
|
+
it("should render a static page fragment", async () => {
|
|
13
|
+
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
14
|
+
|
|
15
|
+
const store = createStore({
|
|
16
|
+
types: { about: module.about },
|
|
17
|
+
updateMode: "manual",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const html = await renderPage(store, module, DEFAULT_OPTIONS)
|
|
21
|
+
|
|
22
|
+
expect(html).toMatchSnapshot()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("should render a whole static page", async () => {
|
|
26
|
+
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
27
|
+
|
|
28
|
+
const store = createStore({
|
|
29
|
+
types: { about: module.about },
|
|
30
|
+
updateMode: "manual",
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const html = await renderPage(store, module, {
|
|
34
|
+
...DEFAULT_OPTIONS,
|
|
35
|
+
wrap: true,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(html).toMatchSnapshot()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should render a page with entity", async () => {
|
|
42
|
+
const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
|
|
43
|
+
|
|
44
|
+
const store = createStore({
|
|
45
|
+
types: { about: module.about },
|
|
46
|
+
entities: { about: { type: "about", name: "Us" } },
|
|
47
|
+
updateMode: "manual",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const html = await renderPage(store, module, DEFAULT_OPTIONS)
|
|
51
|
+
|
|
52
|
+
expect(html).toMatchSnapshot()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("should render a page with pre-fetched data", async () => {
|
|
56
|
+
const module = await import(path.resolve(path.join(PAGES_DIR, "posts.js")))
|
|
57
|
+
|
|
58
|
+
const store = createStore({
|
|
59
|
+
types: { posts: module.posts },
|
|
60
|
+
entities: { posts: { type: "posts", name: "Antony", posts: [] } },
|
|
61
|
+
updateMode: "manual",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const html = await renderPage(store, module, DEFAULT_OPTIONS)
|
|
65
|
+
|
|
66
|
+
expect(html).toMatchSnapshot()
|
|
67
|
+
})
|
package/src/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
2
3
|
|
|
3
4
|
import { glob } from "glob"
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ export async function getPages(pagesDir = "pages") {
|
|
|
20
21
|
if (isDynamic(route.pattern)) {
|
|
21
22
|
// Dynamic route - call getStaticPaths if it exists
|
|
22
23
|
try {
|
|
23
|
-
const module = await import(path.resolve(route.filePath))
|
|
24
|
+
const module = await import(pathToFileURL(path.resolve(route.filePath)))
|
|
24
25
|
|
|
25
26
|
if (typeof module.getStaticPaths === "function") {
|
|
26
27
|
const paths = await module.getStaticPaths()
|
|
@@ -35,6 +36,7 @@ export async function getPages(pagesDir = "pages") {
|
|
|
35
36
|
|
|
36
37
|
pages.push({
|
|
37
38
|
path: urlPath,
|
|
39
|
+
modulePath: route.modulePath,
|
|
38
40
|
filePath: route.filePath,
|
|
39
41
|
params,
|
|
40
42
|
})
|
|
@@ -52,6 +54,7 @@ export async function getPages(pagesDir = "pages") {
|
|
|
52
54
|
// Static route - add directly
|
|
53
55
|
pages.push({
|
|
54
56
|
path: route.pattern === "" ? "/" : route.pattern,
|
|
57
|
+
modulePath: route.modulePath,
|
|
55
58
|
filePath: route.filePath,
|
|
56
59
|
params: {},
|
|
57
60
|
})
|
|
@@ -99,7 +102,8 @@ export async function getRoutes(pagesDir = "pages") {
|
|
|
99
102
|
// Find all .js and .ts files in pages directory
|
|
100
103
|
const files = await glob("**/*.{js,ts}", {
|
|
101
104
|
cwd: pagesDir,
|
|
102
|
-
ignore: ["
|
|
105
|
+
ignore: ["**/*.test.{js,ts}", "**/*.spec.{js,ts}"],
|
|
106
|
+
posix: true,
|
|
103
107
|
})
|
|
104
108
|
|
|
105
109
|
const routes = files.map((file) => {
|
|
@@ -109,6 +113,7 @@ export async function getRoutes(pagesDir = "pages") {
|
|
|
109
113
|
|
|
110
114
|
return {
|
|
111
115
|
pattern,
|
|
116
|
+
modulePath: file,
|
|
112
117
|
filePath,
|
|
113
118
|
regex,
|
|
114
119
|
params,
|
|
@@ -138,8 +143,8 @@ function filePathToPattern(file) {
|
|
|
138
143
|
.replace(/\.(js|ts)$/, "") // Remove extension
|
|
139
144
|
.replace(/\/index$/, "") // index becomes root of directory
|
|
140
145
|
.replace(/^index$/, "") // Handle root index
|
|
141
|
-
.replace(
|
|
142
|
-
.replace(
|
|
146
|
+
.replace(/__(\w+)/g, "*") // __path becomes *
|
|
147
|
+
.replace(/_(\w+)/g, ":$1") // _id becomes :id
|
|
143
148
|
|
|
144
149
|
// Normalize to start with /
|
|
145
150
|
return "/" + pattern.replace(/^\//, "")
|
package/src/router.test.js
CHANGED
|
@@ -38,7 +38,7 @@ describe("router", () => {
|
|
|
38
38
|
// Root usually comes after specific paths but before catch-all if it was a catch-all root,
|
|
39
39
|
// but here / is static.
|
|
40
40
|
// Let's just check that we found them.
|
|
41
|
-
expect(routes).toHaveLength(
|
|
41
|
+
expect(routes).toHaveLength(6)
|
|
42
42
|
})
|
|
43
43
|
})
|
|
44
44
|
|
|
@@ -90,21 +90,7 @@ describe("router", () => {
|
|
|
90
90
|
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
91
91
|
const pages = await getPages(FIXTURES_DIR)
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
expect(pages).toContainEqual(
|
|
95
|
-
expect.objectContaining({ path: "/", params: {} }),
|
|
96
|
-
)
|
|
97
|
-
expect(pages).toContainEqual(
|
|
98
|
-
expect.objectContaining({ path: "/about", params: {} }),
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
// Dynamic routes with getStaticPaths
|
|
102
|
-
expect(pages).toContainEqual(
|
|
103
|
-
expect.objectContaining({ path: "/posts/1", params: { id: "1" } }),
|
|
104
|
-
)
|
|
105
|
-
expect(pages).toContainEqual(
|
|
106
|
-
expect.objectContaining({ path: "/posts/2", params: { id: "2" } }),
|
|
107
|
-
)
|
|
93
|
+
expect(pages).toMatchSnapshot()
|
|
108
94
|
|
|
109
95
|
// Dynamic route without getStaticPaths should be skipped (and warn)
|
|
110
96
|
const blogPage = pages.find((p) => p.path.includes("/blog/"))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getModuleName } from "../module.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate the code that goes inside the <!-- SSX --> marker.
|
|
5
|
+
* This creates the types and entities objects for the client-side store.
|
|
6
|
+
*/
|
|
7
|
+
export function generateApp(store, renderedPages) {
|
|
8
|
+
// Collect all unique page modules and their exports
|
|
9
|
+
const pageImports = new Map()
|
|
10
|
+
const routeEntries = []
|
|
11
|
+
|
|
12
|
+
for (const { page, module } of renderedPages) {
|
|
13
|
+
const importPath = "@/pages/" + page.modulePath
|
|
14
|
+
|
|
15
|
+
const exportName = getModuleName(module)
|
|
16
|
+
|
|
17
|
+
pageImports.set(importPath, exportName)
|
|
18
|
+
|
|
19
|
+
routeEntries.push(` '${page.path}': '${exportName}'`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Generate import statements
|
|
23
|
+
const imports = Array.from(pageImports.entries())
|
|
24
|
+
.map(
|
|
25
|
+
([importPath, exportName]) =>
|
|
26
|
+
`import { ${exportName} } from '${importPath}'`,
|
|
27
|
+
)
|
|
28
|
+
.join("\n")
|
|
29
|
+
|
|
30
|
+
// Generate routes object
|
|
31
|
+
const routes = routeEntries.join(",\n")
|
|
32
|
+
|
|
33
|
+
// Generate type registrations
|
|
34
|
+
const typeEntries = Array.from(pageImports.values())
|
|
35
|
+
.map((name) => ` ${name}`)
|
|
36
|
+
.join(",\n")
|
|
37
|
+
|
|
38
|
+
return `import { createDevtools, createStore, mount, router } from "@inglorious/web"
|
|
39
|
+
${imports}
|
|
40
|
+
|
|
41
|
+
const types = {
|
|
42
|
+
router,
|
|
43
|
+
${typeEntries}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entities = {
|
|
47
|
+
router: {
|
|
48
|
+
type: 'router',
|
|
49
|
+
routes: {
|
|
50
|
+
${routes}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
${JSON.stringify(store.getState(), null, 2).slice(1, -1)}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const middlewares = []
|
|
57
|
+
if (import.meta.env.DEV) {
|
|
58
|
+
middlewares.push(createDevtools().middleware)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const store = createStore({ types, entities, middlewares })
|
|
62
|
+
|
|
63
|
+
const root = document.getElementById("root")
|
|
64
|
+
root.innerHTML = ""
|
|
65
|
+
|
|
66
|
+
mount(store, (api) => {
|
|
67
|
+
const { route } = api.getEntity("router")
|
|
68
|
+
return api.render(route, { allowType: true })
|
|
69
|
+
}, root)`
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { minifyTemplateLiterals } from "rollup-plugin-minify-template-literals"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Vite config for building the client bundle
|
|
7
|
+
*/
|
|
8
|
+
export function createViteConfig(options = {}) {
|
|
9
|
+
const { rootDir = "src", outDir = "dist" } = options
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
root: process.cwd(),
|
|
13
|
+
plugins: [minifyTemplateLiterals()],
|
|
14
|
+
build: {
|
|
15
|
+
outDir,
|
|
16
|
+
emptyOutDir: false, // Don't delete HTML files we already generated
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
input: {
|
|
19
|
+
main: path.resolve(outDir, "main.js"),
|
|
20
|
+
},
|
|
21
|
+
output: {
|
|
22
|
+
entryFileNames: "[name].js",
|
|
23
|
+
chunkFileNames: "[name].[hash].js",
|
|
24
|
+
assetFileNames: "[name].[ext]",
|
|
25
|
+
manualChunks(id) {
|
|
26
|
+
// if (id.includes("node_modules/@inglorious")) return "inglorious"
|
|
27
|
+
if (id.includes("node_modules")) return "vendor"
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
resolve: {
|
|
33
|
+
alias: {
|
|
34
|
+
"@": path.resolve(process.cwd(), rootDir),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,193 +0,0 @@
|
|
|
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><script> & "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
|
-
`;
|