@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 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 ready** - Full type support (coming soon)
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
- ### �️ Sitemap & RSS Generation
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
- ### οΏ½πŸ“ File-Based Routing
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
- - [ ] TypeScript support
638
+ - [x] TypeScript support
613
639
  - [ ] Image optimization
614
640
  - [ ] API routes (serverless functions)
615
641
  - [ ] MDX support
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ssx",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",
@@ -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
  })
@@ -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(store, pagesToChange, mergedOptions)
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
 
@@ -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 import(pathToFileURL(path.resolve(page.filePath)))
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 viteServer.ssrLoadModule(page.filePath)
63
+ const module = await loader(page.filePath)
63
64
  page.module = module
64
65
 
65
66
  const entity = store._api.getEntity(page.moduleName)
@@ -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 import(pathToFileURL(path.resolve(route.filePath)))
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 *
@@ -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 import(pathToFileURL(page.filePath))
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
- try {
30
- const module = await import(
31
- pathToFileURL(path.join(rootDir, "entities.js"))
32
- )
33
- entities = module.entities
34
- } catch {
35
- entities = {}
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" })
@@ -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(__dirname, "..", "__fixtures__")
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
  })