@inglorious/ssx 1.1.1 β 1.2.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/README.md +30 -4
- package/package.json +1 -1
- package/src/build/build.test.js +9 -2
- package/src/build/index.js +18 -4
- package/src/build/pages.js +4 -2
- package/src/dev/index.js +8 -7
- package/src/router/index.js +6 -4
- package/src/store/index.js +14 -9
- package/src/store/store.test.js +20 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ SSX takes your entity-based web apps and generates optimized static HTML with fu
|
|
|
30
30
|
- **Hot reload dev server** - See changes instantly
|
|
31
31
|
- **Lazy-loaded routes** - Code splitting automatically
|
|
32
32
|
- **lit-html hydration** - Interactive UI without the bloat
|
|
33
|
-
- **TypeScript
|
|
33
|
+
- **TypeScript Ready** - Write your pages and entities in TypeScript.
|
|
34
34
|
|
|
35
35
|
### π Production Ready
|
|
36
36
|
|
|
@@ -59,6 +59,32 @@ npm run dev
|
|
|
59
59
|
|
|
60
60
|
Or manually: -->
|
|
61
61
|
|
|
62
|
+
### Create Your First Site (TypeScript)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// src/pages/index.ts
|
|
66
|
+
import { html } from "@inglorious/web"
|
|
67
|
+
|
|
68
|
+
// You can import API for type safety, though it's optional
|
|
69
|
+
// import type { API } from "@inglorious/web"
|
|
70
|
+
|
|
71
|
+
export const index = {
|
|
72
|
+
render(/* entity: any, api: API */) {
|
|
73
|
+
return html`
|
|
74
|
+
<div>
|
|
75
|
+
<h1>Welcome to SSX!</h1>
|
|
76
|
+
<p>This page was pre-rendered at build time.</p>
|
|
77
|
+
<nav>
|
|
78
|
+
<a href="/about">About</a>
|
|
79
|
+
</nav>
|
|
80
|
+
</div>
|
|
81
|
+
`
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Create Your First Site (JavaScript)
|
|
87
|
+
|
|
62
88
|
```javascript
|
|
63
89
|
// src/pages/index.js
|
|
64
90
|
import { html } from "@inglorious/web"
|
|
@@ -119,7 +145,7 @@ Deploy `dist/` to:
|
|
|
119
145
|
|
|
120
146
|
## Features
|
|
121
147
|
|
|
122
|
-
###
|
|
148
|
+
### πΊοΈ Sitemap & RSS Generation
|
|
123
149
|
|
|
124
150
|
SSX automatically generates `sitemap.xml` and `rss.xml` based on your pages. Configure them in `src/site.config.js`:
|
|
125
151
|
|
|
@@ -159,7 +185,7 @@ export default {
|
|
|
159
185
|
|
|
160
186
|
Pages with a `published` date in metadata are included in RSS feeds.
|
|
161
187
|
|
|
162
|
-
###
|
|
188
|
+
### π File-Based Routing
|
|
163
189
|
|
|
164
190
|
Your file structure defines your routes:
|
|
165
191
|
|
|
@@ -609,7 +635,7 @@ Check out these example projects:
|
|
|
609
635
|
|
|
610
636
|
## Roadmap
|
|
611
637
|
|
|
612
|
-
- [
|
|
638
|
+
- [x] TypeScript support
|
|
613
639
|
- [ ] Image optimization
|
|
614
640
|
- [ ] API routes (serverless functions)
|
|
615
641
|
- [ ] MDX support
|
package/package.json
CHANGED
package/src/build/build.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
|
|
4
|
-
import { build as viteBuild } from "vite"
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
4
|
+
import { build as viteBuild, createServer } from "vite"
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
6
6
|
|
|
7
7
|
import { getPages } from "../router/index.js"
|
|
8
8
|
import { generateApp } from "../scripts/app.js"
|
|
@@ -39,6 +39,13 @@ describe("build", () => {
|
|
|
39
39
|
// Mock console to keep output clean
|
|
40
40
|
vi.spyOn(console, "log").mockImplementation(() => {})
|
|
41
41
|
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
createServer.mockResolvedValue({
|
|
44
|
+
ssrLoadModule: vi.fn(),
|
|
45
|
+
close: vi.fn(),
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
42
49
|
afterEach(() => {
|
|
43
50
|
vi.clearAllMocks()
|
|
44
51
|
})
|
package/src/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
|
|
4
|
-
import { build as viteBuild } from "vite"
|
|
4
|
+
import { build as viteBuild, createServer } from "vite"
|
|
5
5
|
|
|
6
6
|
import { getPages } from "../router/index.js"
|
|
7
7
|
import { generateApp } from "../scripts/app.js"
|
|
@@ -47,8 +47,16 @@ export async function build(options = {}) {
|
|
|
47
47
|
|
|
48
48
|
console.log("π¨ Starting build...\n")
|
|
49
49
|
|
|
50
|
+
// Create a temporary Vite server to load modules (supports TS)
|
|
51
|
+
const vite = await createServer({
|
|
52
|
+
...createViteConfig(mergedOptions),
|
|
53
|
+
server: { middlewareMode: true, hmr: false },
|
|
54
|
+
appType: "custom",
|
|
55
|
+
})
|
|
56
|
+
const loader = (p) => vite.ssrLoadModule(p)
|
|
57
|
+
|
|
50
58
|
// 0. Get all pages to build (Fail fast if source is broken)
|
|
51
|
-
const allPages = await getPages(path.join(rootDir, "pages"))
|
|
59
|
+
const allPages = await getPages(path.join(rootDir, "pages"), loader)
|
|
52
60
|
console.log(`π Found ${allPages.length} pages\n`)
|
|
53
61
|
|
|
54
62
|
// Load previous build manifest
|
|
@@ -85,10 +93,15 @@ export async function build(options = {}) {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
// 4. Generate store with all types and initial entities
|
|
88
|
-
const store = await generateStore(allPages, mergedOptions)
|
|
96
|
+
const store = await generateStore(allPages, mergedOptions, loader)
|
|
89
97
|
|
|
90
98
|
// 5. Render only pages that changed
|
|
91
|
-
const changedPages = await generatePages(
|
|
99
|
+
const changedPages = await generatePages(
|
|
100
|
+
store,
|
|
101
|
+
pagesToChange,
|
|
102
|
+
mergedOptions,
|
|
103
|
+
loader,
|
|
104
|
+
)
|
|
92
105
|
// For skipped pages, load their metadata from disk if needed for sitemap/RSS
|
|
93
106
|
const skippedPages = await generatePages(store, pagesToSkip, {
|
|
94
107
|
...mergedOptions,
|
|
@@ -133,6 +146,7 @@ export async function build(options = {}) {
|
|
|
133
146
|
const viteConfig = createViteConfig(mergedOptions)
|
|
134
147
|
await viteBuild(viteConfig)
|
|
135
148
|
|
|
149
|
+
await vite.close()
|
|
136
150
|
// 12. Cleanup
|
|
137
151
|
// console.log("\nπ§Ή Cleaning up...\n")
|
|
138
152
|
|
package/src/build/pages.js
CHANGED
|
@@ -14,10 +14,12 @@ import { extractPageMetadata } from "./metadata.js"
|
|
|
14
14
|
* @param {Object} [options] - Generation options.
|
|
15
15
|
* @param {boolean} [options.shouldGenerateHtml=true] - Whether to generate HTML.
|
|
16
16
|
* @param {boolean} [options.shouldGenerateMetadata=true] - Whether to generate metadata.
|
|
17
|
+
* @param {Function} [loader] - Optional loader function.
|
|
17
18
|
* @returns {Promise<Array<Object>>} The processed pages with `html` and `metadata` properties added.
|
|
18
19
|
*/
|
|
19
|
-
export async function generatePages(store, pages, options = {}) {
|
|
20
|
+
export async function generatePages(store, pages, options = {}, loader) {
|
|
20
21
|
const { shouldGenerateHtml = true, shouldGenerateMetadata = true } = options
|
|
22
|
+
const load = loader || ((p) => import(pathToFileURL(path.resolve(p))))
|
|
21
23
|
|
|
22
24
|
const api = store._api
|
|
23
25
|
|
|
@@ -26,7 +28,7 @@ export async function generatePages(store, pages, options = {}) {
|
|
|
26
28
|
` Generating ${shouldGenerateHtml ? "HTML" : ""}${shouldGenerateHtml && shouldGenerateMetadata ? " and " : ""}${shouldGenerateMetadata ? "metadata" : ""} for ${page.path}...`,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
const module = await
|
|
31
|
+
const module = await load(page.filePath)
|
|
30
32
|
page.module = module
|
|
31
33
|
|
|
32
34
|
const entity = api.getEntity(page.moduleName)
|
package/src/dev/index.js
CHANGED
|
@@ -25,16 +25,17 @@ export async function dev(options = {}) {
|
|
|
25
25
|
|
|
26
26
|
console.log("π Starting dev server...\n")
|
|
27
27
|
|
|
28
|
+
// Create Vite dev server
|
|
29
|
+
const viteConfig = createViteConfig(mergedOptions)
|
|
30
|
+
const viteServer = await createServer(viteConfig)
|
|
31
|
+
const loader = (p) => viteServer.ssrLoadModule(p)
|
|
32
|
+
|
|
28
33
|
// Get all pages once at startup
|
|
29
|
-
const pages = await getPages(path.join(rootDir, "pages"))
|
|
34
|
+
const pages = await getPages(path.join(rootDir, "pages"), loader)
|
|
30
35
|
console.log(`π Found ${pages.length} pages\n`)
|
|
31
36
|
|
|
32
37
|
// Generate store config once for all pages
|
|
33
|
-
const store = await generateStore(pages, mergedOptions)
|
|
34
|
-
|
|
35
|
-
// Create Vite dev server
|
|
36
|
-
const viteConfig = createViteConfig(mergedOptions)
|
|
37
|
-
const viteServer = await createServer(viteConfig)
|
|
38
|
+
const store = await generateStore(pages, mergedOptions, loader)
|
|
38
39
|
|
|
39
40
|
// Use Vite's middleware first (handles HMR, static files, etc.)
|
|
40
41
|
const connectServer = connect()
|
|
@@ -59,7 +60,7 @@ export async function dev(options = {}) {
|
|
|
59
60
|
const page = pages.find((p) => matchRoute(p.path, url))
|
|
60
61
|
if (!page) return next()
|
|
61
62
|
|
|
62
|
-
const module = await
|
|
63
|
+
const module = await loader(page.filePath)
|
|
63
64
|
page.module = module
|
|
64
65
|
|
|
65
66
|
const entity = store._api.getEntity(page.moduleName)
|
package/src/router/index.js
CHANGED
|
@@ -17,15 +17,17 @@ const SCORE_MULTIPLIER = 0.1
|
|
|
17
17
|
* to generate all possible paths.
|
|
18
18
|
*
|
|
19
19
|
* @param {string} pagesDir - The directory containing page files.
|
|
20
|
+
* @param {Function} [loader] - Optional loader function (e.g. vite.ssrLoadModule).
|
|
20
21
|
* @returns {Promise<Array<Object>>} A list of page objects with metadata.
|
|
21
22
|
*/
|
|
22
|
-
export async function getPages(pagesDir = "pages") {
|
|
23
|
+
export async function getPages(pagesDir = "pages", loader) {
|
|
23
24
|
const routes = await getRoutes(pagesDir)
|
|
24
25
|
const pages = []
|
|
26
|
+
const load = loader || ((p) => import(pathToFileURL(path.resolve(p))))
|
|
25
27
|
|
|
26
28
|
for (const route of routes) {
|
|
27
29
|
try {
|
|
28
|
-
const module = await
|
|
30
|
+
const module = await load(route.filePath)
|
|
29
31
|
const moduleName = getModuleName(module)
|
|
30
32
|
|
|
31
33
|
if (isDynamic(route.pattern)) {
|
|
@@ -122,7 +124,7 @@ export async function resolvePage(url, pagesDir = "pages") {
|
|
|
122
124
|
*/
|
|
123
125
|
export async function getRoutes(pagesDir = "pages") {
|
|
124
126
|
// Find all .js and .ts files in pages directory
|
|
125
|
-
const files = await glob("**/*.{js,ts}", {
|
|
127
|
+
const files = await glob("**/*.{js,ts,jsx,tsx}", {
|
|
126
128
|
cwd: pagesDir,
|
|
127
129
|
ignore: ["**/*.test.{js,ts}", "**/*.spec.{js,ts}"],
|
|
128
130
|
posix: true,
|
|
@@ -190,7 +192,7 @@ export function matchRoute(pattern, url) {
|
|
|
190
192
|
function filePathToPattern(file) {
|
|
191
193
|
let pattern = file
|
|
192
194
|
.replace(/\\/g, "/")
|
|
193
|
-
.replace(/\.(js|ts)$/, "") // Remove extension
|
|
195
|
+
.replace(/\.(js|ts|jsx|tsx)$/, "") // Remove extension
|
|
194
196
|
.replace(/\/index$/, "") // index becomes root of directory
|
|
195
197
|
.replace(/^index$/, "") // Handle root index
|
|
196
198
|
.replace(/__(\w+)/g, "*") // __path becomes *
|
package/src/store/index.js
CHANGED
|
@@ -13,26 +13,31 @@ import { getModuleName } from "../utils/module.js"
|
|
|
13
13
|
* @param {Array<Object>} pages - List of page objects containing file paths.
|
|
14
14
|
* @param {Object} options - Configuration options.
|
|
15
15
|
* @param {string} [options.rootDir="src"] - Root directory to look for entities.js.
|
|
16
|
+
* @param {Function} [loader] - Optional loader function.
|
|
16
17
|
* @returns {Promise<Object>} The initialized store instance.
|
|
17
18
|
*/
|
|
18
|
-
export async function generateStore(pages = [], options = {}) {
|
|
19
|
+
export async function generateStore(pages = [], options = {}, loader) {
|
|
19
20
|
const { rootDir = "src" } = options
|
|
21
|
+
const load = loader || ((p) => import(pathToFileURL(p)))
|
|
20
22
|
|
|
21
23
|
const types = {}
|
|
22
24
|
for (const page of pages) {
|
|
23
|
-
const pageModule = await
|
|
25
|
+
const pageModule = await load(page.filePath)
|
|
24
26
|
const name = getModuleName(pageModule)
|
|
25
27
|
types[name] = pageModule[name]
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
let entities = {}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const extensions = ["js", "ts"]
|
|
32
|
+
|
|
33
|
+
for (const ext of extensions) {
|
|
34
|
+
try {
|
|
35
|
+
const module = await load(path.join(rootDir, `entities.${ext}`))
|
|
36
|
+
entities = module.entities
|
|
37
|
+
break
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore and try next extension
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
return createStore({ types, entities, updateMode: "manual" })
|
package/src/store/store.test.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
|
|
3
|
-
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { describe, expect, it, vi } from "vitest"
|
|
4
4
|
|
|
5
5
|
import { generateStore } from "."
|
|
6
6
|
|
|
7
|
-
const ROOT_DIR = path.join(
|
|
7
|
+
const ROOT_DIR = path.join(import.meta.dirname, "..", "__fixtures__")
|
|
8
8
|
|
|
9
9
|
describe("generateStore", () => {
|
|
10
10
|
it("should generate the proper types and entities from a static page", async () => {
|
|
@@ -53,4 +53,22 @@ describe("generateStore", () => {
|
|
|
53
53
|
// Should initialize with empty entities (or at least not the ones from fixtures)
|
|
54
54
|
expect(store.getState()).not.toHaveProperty("about")
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
it("should attempt to load entities.js and entities.ts", async () => {
|
|
58
|
+
const loader = vi.fn(async (p) => {
|
|
59
|
+
// Mock a successful page load
|
|
60
|
+
if (p.endsWith("index.js")) {
|
|
61
|
+
return { index: { render: () => {} } }
|
|
62
|
+
}
|
|
63
|
+
// Mock entity files not being found
|
|
64
|
+
throw new Error("MODULE_NOT_FOUND")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const page = { filePath: path.join(ROOT_DIR, "pages", "index.js") }
|
|
68
|
+
await generateStore([page], { rootDir: "src" }, loader)
|
|
69
|
+
|
|
70
|
+
expect(loader).toHaveBeenCalledWith(page.filePath)
|
|
71
|
+
expect(loader).toHaveBeenCalledWith(path.join("src", "entities.js"))
|
|
72
|
+
expect(loader).toHaveBeenCalledWith(path.join("src", "entities.ts"))
|
|
73
|
+
})
|
|
56
74
|
})
|