@inglorious/ssx 0.2.2 → 0.3.0
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 +51 -0
- package/package.json +10 -8
- package/src/build.js +2 -20
- package/src/build.test.js +2 -2
- package/src/dev.js +121 -0
- package/src/html.js +2 -2
- package/src/module.test.js +45 -0
- package/src/render.test.js +2 -1
- package/src/router.test.js +9 -8
- package/src/scripts/app.js +12 -41
- package/src/scripts/app.test.js +47 -0
- package/src/scripts/main.js +2 -2
- package/src/store.js +23 -0
- package/src/store.test.js +40 -0
- package/src/vite-config.js +1 -5
package/bin/ssx.js
CHANGED
|
@@ -23,6 +23,57 @@ program
|
|
|
23
23
|
.description("Static Site Xecution for @inglorious/web")
|
|
24
24
|
.version(packageJson.version)
|
|
25
25
|
|
|
26
|
+
program
|
|
27
|
+
.command("dev")
|
|
28
|
+
.description("Start development server with hot reload")
|
|
29
|
+
.option("-r, --root <dir>", "source root directory", "src")
|
|
30
|
+
.option("-p, --port <port>", "dev server port", 3000)
|
|
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 dev
|
|
58
|
+
const { dev } = await import("../src/dev.js")
|
|
59
|
+
|
|
60
|
+
await dev({
|
|
61
|
+
rootDir: path.resolve(cwd, options.root),
|
|
62
|
+
port: Number(options.port),
|
|
63
|
+
renderOptions: {
|
|
64
|
+
seed: Number(options.seed),
|
|
65
|
+
title: options.title,
|
|
66
|
+
meta: {},
|
|
67
|
+
styles: options.styles || [],
|
|
68
|
+
scripts: options.scripts || [],
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Dev server failed:", error)
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
26
77
|
program
|
|
27
78
|
.command("build")
|
|
28
79
|
.description("Build static site from pages directory")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ssx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -34,22 +34,23 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
36
36
|
"src",
|
|
37
|
-
"!src
|
|
38
|
-
"!src
|
|
37
|
+
"!src/**/__fixtures__",
|
|
38
|
+
"!src/**/__snapshots__"
|
|
39
39
|
],
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"commander": "^14.0.2",
|
|
45
|
+
"connect": "^3.7.0",
|
|
45
46
|
"glob": "^13.0.0",
|
|
46
47
|
"happy-dom": "^20.0.11",
|
|
47
|
-
"
|
|
48
|
+
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
49
|
+
"vite": "^7.1.3",
|
|
50
|
+
"@inglorious/web": "3.0.1"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"prettier": "^3.6.2",
|
|
51
|
-
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
52
|
-
"vite": "^7.1.3",
|
|
53
54
|
"vitest": "^1.6.1",
|
|
54
55
|
"@inglorious/eslint-config": "1.1.1"
|
|
55
56
|
},
|
|
@@ -61,7 +62,8 @@
|
|
|
61
62
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
62
63
|
"test:watch": "vitest",
|
|
63
64
|
"test": "vitest run",
|
|
64
|
-
"dev": "node ./bin/ssx.js
|
|
65
|
-
"
|
|
65
|
+
"dev": "node ./bin/ssx.js dev -r ./src/__fixtures__",
|
|
66
|
+
"build": "node ./bin/ssx.js build -r ./src/__fixtures__",
|
|
67
|
+
"preview": "pnpm dlx serve dist"
|
|
66
68
|
}
|
|
67
69
|
}
|
package/src/build.js
CHANGED
|
@@ -2,15 +2,14 @@ import fs from "node:fs/promises"
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { pathToFileURL } from "node:url"
|
|
4
4
|
|
|
5
|
-
import { createStore } from "@inglorious/web"
|
|
6
5
|
import { build as viteBuild } from "vite"
|
|
7
6
|
|
|
8
|
-
import { getModuleName } from "./module.js"
|
|
9
7
|
import { renderPage } from "./render.js"
|
|
10
8
|
import { getPages } from "./router.js"
|
|
11
9
|
import { generateApp } from "./scripts/app.js"
|
|
12
10
|
import { generateLitLoader } from "./scripts/lit-loader.js"
|
|
13
11
|
import { generateMain } from "./scripts/main.js"
|
|
12
|
+
import { generateStore } from "./store.js"
|
|
14
13
|
import { createViteConfig } from "./vite-config.js"
|
|
15
14
|
|
|
16
15
|
export async function build(options = {}) {
|
|
@@ -39,7 +38,7 @@ export async function build(options = {}) {
|
|
|
39
38
|
const litLoader = generateLitLoader(renderOptions)
|
|
40
39
|
await fs.writeFile(path.join(outDir, "lit-loader.js"), litLoader, "utf-8")
|
|
41
40
|
|
|
42
|
-
const app = generateApp(store,
|
|
41
|
+
const app = generateApp(store, pages)
|
|
43
42
|
await fs.writeFile(path.join(outDir, "app.js"), app, "utf-8")
|
|
44
43
|
|
|
45
44
|
const main = generateMain()
|
|
@@ -65,23 +64,6 @@ export async function build(options = {}) {
|
|
|
65
64
|
return { pages: renderedPages.length, outDir }
|
|
66
65
|
}
|
|
67
66
|
|
|
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
67
|
async function generatePages(pages, options = {}) {
|
|
86
68
|
const { renderOptions } = options
|
|
87
69
|
|
package/src/build.test.js
CHANGED
|
@@ -4,8 +4,8 @@ import { it } from "vitest"
|
|
|
4
4
|
|
|
5
5
|
import { build } from "./build"
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const ROOT_DIR = path.join(__dirname, "__fixtures__")
|
|
8
8
|
|
|
9
9
|
it.skip("should build full static pages", async () => {
|
|
10
|
-
await build({ rootDir:
|
|
10
|
+
await build({ rootDir: ROOT_DIR })
|
|
11
11
|
})
|
package/src/dev.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import connect from "connect"
|
|
4
|
+
import { createServer } from "vite"
|
|
5
|
+
|
|
6
|
+
import { renderPage } from "./render.js"
|
|
7
|
+
import { getPages } from "./router.js"
|
|
8
|
+
import { generateApp } from "./scripts/app.js"
|
|
9
|
+
import { generateLitLoader } from "./scripts/lit-loader.js"
|
|
10
|
+
import { generateMain } from "./scripts/main.js"
|
|
11
|
+
import { generateStore } from "./store.js"
|
|
12
|
+
|
|
13
|
+
export async function dev(options = {}) {
|
|
14
|
+
const { rootDir = "src", port = 3000, renderOptions = {} } = options
|
|
15
|
+
|
|
16
|
+
console.log("🚀 Starting dev server...\n")
|
|
17
|
+
|
|
18
|
+
// Get all pages once at startup
|
|
19
|
+
const pages = await getPages(path.join(rootDir, "pages"))
|
|
20
|
+
console.log(`📄 Found ${pages.length} pages\n`)
|
|
21
|
+
|
|
22
|
+
// Generate store config once for all pages
|
|
23
|
+
const store = await generateStore(pages, options)
|
|
24
|
+
|
|
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
|
+
// Create Vite dev server
|
|
35
|
+
const viteServer = await createServer({
|
|
36
|
+
root: process.cwd(),
|
|
37
|
+
server: { port, middlewareMode: true },
|
|
38
|
+
appType: "custom",
|
|
39
|
+
plugins: [virtualPlugin()],
|
|
40
|
+
resolve: {
|
|
41
|
+
alias: {
|
|
42
|
+
"@": path.resolve(process.cwd(), rootDir),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Use Vite's middleware first (handles HMR, static files, etc.)
|
|
48
|
+
const connectServer = connect()
|
|
49
|
+
|
|
50
|
+
connectServer.use(viteServer.middlewares)
|
|
51
|
+
|
|
52
|
+
// Add SSR middleware
|
|
53
|
+
connectServer.use(async (req, res, next) => {
|
|
54
|
+
const [url] = req.url.split("?")
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Find matching page
|
|
58
|
+
const page = pages.find((p) => matchRoute(p.path, url))
|
|
59
|
+
if (!page) return next()
|
|
60
|
+
|
|
61
|
+
const store = await generateStore([page], options)
|
|
62
|
+
const module = await viteServer.ssrLoadModule(page.filePath)
|
|
63
|
+
const html = await renderPage(store, module, {
|
|
64
|
+
...renderOptions,
|
|
65
|
+
wrap: true,
|
|
66
|
+
dev: true,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
res.setHeader("Content-Type", "text/html")
|
|
70
|
+
res.end(html)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
viteServer.ssrFixStacktrace(error)
|
|
73
|
+
next(error) // Let Vite handle the error overlay
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const server = connectServer.listen(port)
|
|
78
|
+
|
|
79
|
+
console.log(`\n✨ Dev server running at http://localhost:${port}\n`)
|
|
80
|
+
console.log("Press Ctrl+C to stop\n")
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
close: () => {
|
|
84
|
+
server.close()
|
|
85
|
+
viteServer.close()
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Simple route matcher (could be moved to router.js)
|
|
91
|
+
function matchRoute(pattern, url) {
|
|
92
|
+
const patternParts = pattern.split("/").filter(Boolean)
|
|
93
|
+
const urlParts = url.split("/").filter(Boolean)
|
|
94
|
+
|
|
95
|
+
if (patternParts.length !== urlParts.length) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return patternParts.every((part, i) => {
|
|
100
|
+
if (part.startsWith(":") || part.startsWith("[")) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
return part === urlParts[i]
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const virtualFiles = new Map()
|
|
108
|
+
|
|
109
|
+
function virtualPlugin() {
|
|
110
|
+
return {
|
|
111
|
+
name: "ssx-virtual-files",
|
|
112
|
+
|
|
113
|
+
resolveId(id) {
|
|
114
|
+
if (virtualFiles.has(id)) return id
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
load(id) {
|
|
118
|
+
if (virtualFiles.has(id)) return virtualFiles.get(id)
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/html.js
CHANGED
|
@@ -26,7 +26,7 @@ function stripLitMarkers(html) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function wrapHTML(body, options) {
|
|
29
|
-
const { title = "", meta = {}, styles = [], scripts = [] } = options
|
|
29
|
+
const { dev, title = "", meta = {}, styles = [], scripts = [] } = options
|
|
30
30
|
|
|
31
31
|
return `<!DOCTYPE html>
|
|
32
32
|
<html>
|
|
@@ -41,7 +41,7 @@ function wrapHTML(body, options) {
|
|
|
41
41
|
<body>
|
|
42
42
|
<div id="root">${body}</div>
|
|
43
43
|
|
|
44
|
-
<script type="module" src="/main.js"></script>
|
|
44
|
+
${dev ? `<script type="module" src="/@vite/client"></script>` : ``}<script type="module" src="/main.js"></script>
|
|
45
45
|
${scripts.map((src) => `<script type="module" src="${src}"></script>`).join("\n")}
|
|
46
46
|
</body>
|
|
47
47
|
</html>`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { getModuleName } from "./module"
|
|
4
|
+
|
|
5
|
+
it("should get the name when it's the only export", () => {
|
|
6
|
+
const module = {
|
|
7
|
+
about: {
|
|
8
|
+
render: () => {},
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
expect(getModuleName(module)).toBe("about")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should get the name when there's other exports", () => {
|
|
16
|
+
const module = {
|
|
17
|
+
about: {
|
|
18
|
+
render: () => {},
|
|
19
|
+
},
|
|
20
|
+
title: "About",
|
|
21
|
+
meta: {
|
|
22
|
+
description: "About page",
|
|
23
|
+
},
|
|
24
|
+
scripts: ["/script.js"],
|
|
25
|
+
styles: ["/style.css"],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(getModuleName(module)).toBe("about")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should get the name when the exports are functions", () => {
|
|
32
|
+
const module = {
|
|
33
|
+
about: {
|
|
34
|
+
render: () => {},
|
|
35
|
+
},
|
|
36
|
+
title: () => "About",
|
|
37
|
+
meta: () => ({
|
|
38
|
+
description: "About page",
|
|
39
|
+
}),
|
|
40
|
+
scripts: ["/script.js"],
|
|
41
|
+
styles: ["/style.css"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(getModuleName(module)).toBe("about")
|
|
45
|
+
})
|
package/src/render.test.js
CHANGED
|
@@ -5,7 +5,8 @@ import { expect, it } from "vitest"
|
|
|
5
5
|
|
|
6
6
|
import { renderPage } from "./render"
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const ROOT_DIR = path.join(__dirname, "__fixtures__")
|
|
9
|
+
const PAGES_DIR = path.join(ROOT_DIR, "pages")
|
|
9
10
|
|
|
10
11
|
const DEFAULT_OPTIONS = { stripLitMarkers: true }
|
|
11
12
|
|
package/src/router.test.js
CHANGED
|
@@ -4,7 +4,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"
|
|
|
4
4
|
|
|
5
5
|
import { getPages, getRoutes, resolvePage } from "./router.js"
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const ROOT_DIR = path.join(__dirname, "__fixtures__")
|
|
8
|
+
const PAGES_DIR = path.join(ROOT_DIR, "pages")
|
|
8
9
|
|
|
9
10
|
describe("router", () => {
|
|
10
11
|
afterEach(() => {
|
|
@@ -13,7 +14,7 @@ describe("router", () => {
|
|
|
13
14
|
|
|
14
15
|
describe("getRoutes", () => {
|
|
15
16
|
it("should discover and sort routes correctly", async () => {
|
|
16
|
-
const routes = await getRoutes(
|
|
17
|
+
const routes = await getRoutes(PAGES_DIR)
|
|
17
18
|
|
|
18
19
|
// Expected order based on specificity:
|
|
19
20
|
// 1. /posts/:id (static 'posts' + dynamic 'id') -> score ~4.2
|
|
@@ -44,28 +45,28 @@ describe("router", () => {
|
|
|
44
45
|
|
|
45
46
|
describe("resolvePage", () => {
|
|
46
47
|
it("should resolve root page", async () => {
|
|
47
|
-
const page = await resolvePage("/",
|
|
48
|
+
const page = await resolvePage("/", PAGES_DIR)
|
|
48
49
|
expect(page).not.toBeNull()
|
|
49
50
|
expect(page.filePath).toContain("index.js")
|
|
50
51
|
expect(page.params).toEqual({})
|
|
51
52
|
})
|
|
52
53
|
|
|
53
54
|
it("should resolve static page", async () => {
|
|
54
|
-
const page = await resolvePage("/about",
|
|
55
|
+
const page = await resolvePage("/about", PAGES_DIR)
|
|
55
56
|
expect(page).not.toBeNull()
|
|
56
57
|
expect(page.filePath).toContain("about.js")
|
|
57
58
|
expect(page.params).toEqual({})
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
it("should resolve dynamic page with params", async () => {
|
|
61
|
-
const page = await resolvePage("/blog/hello-world",
|
|
62
|
+
const page = await resolvePage("/blog/hello-world", PAGES_DIR)
|
|
62
63
|
expect(page).not.toBeNull()
|
|
63
64
|
expect(page.filePath).toContain("blog")
|
|
64
65
|
expect(page.params).toEqual({ slug: "hello-world" })
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
it("should resolve catch-all page", async () => {
|
|
68
|
-
const page = await resolvePage("/api/v1/users",
|
|
69
|
+
const page = await resolvePage("/api/v1/users", PAGES_DIR)
|
|
69
70
|
expect(page).not.toBeNull()
|
|
70
71
|
expect(page.filePath).toContain("api")
|
|
71
72
|
expect(page.params).toEqual({ path: "v1/users" })
|
|
@@ -80,7 +81,7 @@ describe("router", () => {
|
|
|
80
81
|
// /about matches /about.
|
|
81
82
|
// / matches /.
|
|
82
83
|
// So /foo should return null.
|
|
83
|
-
const page = await resolvePage("/foo",
|
|
84
|
+
const page = await resolvePage("/foo", PAGES_DIR)
|
|
84
85
|
expect(page).toBeNull()
|
|
85
86
|
})
|
|
86
87
|
})
|
|
@@ -88,7 +89,7 @@ describe("router", () => {
|
|
|
88
89
|
describe("getPages", () => {
|
|
89
90
|
it("should generate static paths for all pages", async () => {
|
|
90
91
|
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
91
|
-
const pages = await getPages(
|
|
92
|
+
const pages = await getPages(PAGES_DIR)
|
|
92
93
|
|
|
93
94
|
expect(pages).toMatchSnapshot()
|
|
94
95
|
|
package/src/scripts/app.js
CHANGED
|
@@ -1,62 +1,33 @@
|
|
|
1
|
-
import { getModuleName } from "../module.js"
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Generate the code that goes inside the <!-- SSX --> marker.
|
|
5
3
|
* This creates the types and entities objects for the client-side store.
|
|
6
4
|
*/
|
|
7
|
-
export function generateApp(store,
|
|
5
|
+
export function generateApp(store, pages) {
|
|
8
6
|
// Collect all unique page modules and their exports
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
7
|
+
const routeEntries = pages.map(
|
|
8
|
+
(page) =>
|
|
9
|
+
` "${page.path}": () => import("@/pages/${page.modulePath}")`,
|
|
10
|
+
)
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.map(
|
|
25
|
-
([importPath, exportName]) =>
|
|
26
|
-
`import { ${exportName} } from '${importPath}'`,
|
|
27
|
-
)
|
|
28
|
-
.join("\n")
|
|
12
|
+
return `import { createDevtools, createStore, mount } from "@inglorious/web"
|
|
13
|
+
import { router } from "@inglorious/web/router"
|
|
29
14
|
|
|
30
|
-
|
|
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
|
-
}
|
|
15
|
+
const types = { router }
|
|
45
16
|
|
|
46
17
|
const entities = {
|
|
47
18
|
router: {
|
|
48
|
-
type:
|
|
19
|
+
type: "router",
|
|
49
20
|
routes: {
|
|
50
|
-
${
|
|
21
|
+
${routeEntries.join(",\n")}
|
|
51
22
|
}
|
|
52
23
|
},
|
|
53
24
|
${JSON.stringify(store.getState(), null, 2).slice(1, -1)}
|
|
54
25
|
}
|
|
55
26
|
|
|
56
27
|
const middlewares = []
|
|
57
|
-
if (import.meta.env.DEV) {
|
|
28
|
+
// if (import.meta.env.DEV) {
|
|
58
29
|
middlewares.push(createDevtools().middleware)
|
|
59
|
-
}
|
|
30
|
+
// }
|
|
60
31
|
|
|
61
32
|
const store = createStore({ types, entities, middlewares })
|
|
62
33
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { expect, it } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { generateStore } from "../store"
|
|
6
|
+
import { generateApp } from "./app"
|
|
7
|
+
|
|
8
|
+
const ROOT_DIR = path.join(__dirname, "..", "__fixtures__")
|
|
9
|
+
|
|
10
|
+
it("should generate the app script for a static page", async () => {
|
|
11
|
+
const page = {
|
|
12
|
+
path: "/",
|
|
13
|
+
modulePath: "index.js",
|
|
14
|
+
filePath: path.join(ROOT_DIR, "pages", "index.js"),
|
|
15
|
+
}
|
|
16
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
17
|
+
|
|
18
|
+
const app = generateApp(store, [page])
|
|
19
|
+
|
|
20
|
+
expect(app).toMatchSnapshot()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("should generate the app script for a page with an entity", async () => {
|
|
24
|
+
const page = {
|
|
25
|
+
path: "/about",
|
|
26
|
+
modulePath: "about.js",
|
|
27
|
+
filePath: path.join(ROOT_DIR, "pages", "about.js"),
|
|
28
|
+
}
|
|
29
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
30
|
+
|
|
31
|
+
const app = generateApp(store, [page])
|
|
32
|
+
|
|
33
|
+
expect(app).toMatchSnapshot()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should generate the app script for a page that has metadata", async () => {
|
|
37
|
+
const page = {
|
|
38
|
+
path: "/posts",
|
|
39
|
+
modulePath: "posts.js",
|
|
40
|
+
filePath: path.join(ROOT_DIR, "pages", "posts.js"),
|
|
41
|
+
}
|
|
42
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
43
|
+
|
|
44
|
+
const app = generateApp(store, [page])
|
|
45
|
+
|
|
46
|
+
expect(app).toMatchSnapshot()
|
|
47
|
+
})
|
package/src/scripts/main.js
CHANGED
package/src/store.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
|
|
4
|
+
import { createStore } from "@inglorious/web"
|
|
5
|
+
|
|
6
|
+
import { getModuleName } from "./module.js"
|
|
7
|
+
|
|
8
|
+
export async function generateStore(pages = [], options = {}) {
|
|
9
|
+
const { rootDir = "src" } = options
|
|
10
|
+
|
|
11
|
+
const types = {}
|
|
12
|
+
for (const page of pages) {
|
|
13
|
+
const pageModule = await import(pathToFileURL(page.filePath))
|
|
14
|
+
const name = getModuleName(pageModule)
|
|
15
|
+
types[name] = pageModule[name]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { entities } = await import(
|
|
19
|
+
pathToFileURL(path.join(rootDir, "entities.js"))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return createStore({ types, entities, updateMode: "manual" })
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { expect, it } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { generateStore } from "./store"
|
|
6
|
+
|
|
7
|
+
const ROOT_DIR = path.join(__dirname, "__fixtures__")
|
|
8
|
+
|
|
9
|
+
it("should generate the proper types and entities from a static page", async () => {
|
|
10
|
+
const page = {
|
|
11
|
+
filePath: path.join(ROOT_DIR, "pages", "index.js"),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
15
|
+
|
|
16
|
+
expect(store.getType("index").render).toBeDefined()
|
|
17
|
+
expect(store.getState()).toMatchSnapshot()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("should generate the proper types and entities from a page with an entity", async () => {
|
|
21
|
+
const page = {
|
|
22
|
+
filePath: path.join(ROOT_DIR, "pages", "about.js"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
26
|
+
|
|
27
|
+
expect(store.getType("about").render).toBeDefined()
|
|
28
|
+
expect(store.getState()).toMatchSnapshot()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should generate the proper types and entities from a page that has metadata", async () => {
|
|
32
|
+
const page = {
|
|
33
|
+
filePath: path.join(ROOT_DIR, "pages", "posts.js"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const store = await generateStore([page], { rootDir: ROOT_DIR })
|
|
37
|
+
|
|
38
|
+
expect(store.getType("posts").render).toBeDefined()
|
|
39
|
+
expect(store.getState()).toMatchSnapshot()
|
|
40
|
+
})
|
package/src/vite-config.js
CHANGED
|
@@ -9,7 +9,7 @@ export function createViteConfig(options = {}) {
|
|
|
9
9
|
const { rootDir = "src", outDir = "dist" } = options
|
|
10
10
|
|
|
11
11
|
return {
|
|
12
|
-
root:
|
|
12
|
+
root: outDir,
|
|
13
13
|
plugins: [minifyTemplateLiterals()],
|
|
14
14
|
build: {
|
|
15
15
|
outDir,
|
|
@@ -22,10 +22,6 @@ export function createViteConfig(options = {}) {
|
|
|
22
22
|
entryFileNames: "[name].js",
|
|
23
23
|
chunkFileNames: "[name].[hash].js",
|
|
24
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
25
|
},
|
|
30
26
|
},
|
|
31
27
|
},
|