@inglorious/ssx 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ssx.js CHANGED
@@ -27,8 +27,8 @@ program
27
27
  program
28
28
  .command("dev")
29
29
  .description("Start development server with hot reload")
30
- .option("-c, --config <file>", "config file path", "site.config.js")
31
- .option("-r, --root <dir>", "source root directory", "src")
30
+ .option("-c, --config <file>", "config file name", "site.config.js")
31
+ .option("-r, --root <dir>", "root directory", ".")
32
32
  .option("-p, --port <port>", "dev server port", 3000)
33
33
  .action(async (options) => {
34
34
  const cwd = process.cwd()
@@ -54,8 +54,8 @@ program
54
54
  program
55
55
  .command("build")
56
56
  .description("Build site from pages directory")
57
- .option("-c, --config <file>", "config file path", "site.config.js")
58
- .option("-r, --root <dir>", "source root directory", "src")
57
+ .option("-c, --config <file>", "config file name", "site.config.js")
58
+ .option("-r, --root <dir>", "root directory", ".")
59
59
  .option("-o, --out <dir>", "output directory", "dist")
60
60
  .option("-i, --incremental", "enable incremental builds", true)
61
61
  .option("-f, --force", "force clean build (ignore cache)", false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ssx",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Server-Side-X. Xecution? Xperience? Who knows.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@ export async function build(options = {}) {
37
37
 
38
38
  const mergedOptions = { ...config, ...options }
39
39
  const {
40
- rootDir = "src",
40
+ rootDir = ".",
41
41
  outDir = "dist",
42
42
  incremental = true,
43
43
  clean = false,
@@ -45,6 +45,8 @@ export async function build(options = {}) {
45
45
  rss,
46
46
  } = mergedOptions
47
47
 
48
+ const pagesDir = path.join(rootDir, "src", "pages")
49
+
48
50
  console.log("🔨 Starting build...\n")
49
51
 
50
52
  // Create a temporary Vite server to load modules (supports TS)
@@ -56,7 +58,7 @@ export async function build(options = {}) {
56
58
  const loader = (p) => vite.ssrLoadModule(p)
57
59
 
58
60
  // 0. Get all pages to build (Fail fast if source is broken)
59
- const allPages = await getPages(path.join(rootDir, "pages"), loader)
61
+ const allPages = await getPages(pagesDir, loader)
60
62
  console.log(`📄 Found ${allPages.length} pages\n`)
61
63
 
62
64
  // Load previous build manifest
@@ -11,18 +11,15 @@ import { markdownPlugin } from "../utils/markdown.js"
11
11
  * Generate Vite config for building the client bundle
12
12
  */
13
13
  export function createViteConfig(options = {}) {
14
- const {
15
- rootDir = "src",
16
- outDir = "dist",
17
- publicDir = "public",
18
- vite = {},
19
- markdown = {},
20
- } = options
14
+ const { rootDir = ".", outDir = "dist", vite = {}, markdown = {} } = options
15
+
16
+ const srcDir = path.resolve(process.cwd(), rootDir, "src")
17
+ const publicDir = path.resolve(process.cwd(), rootDir, "public")
21
18
 
22
19
  return mergeConfig(
23
20
  {
24
- root: rootDir,
25
- publicDir: path.resolve(process.cwd(), rootDir, publicDir),
21
+ root: process.cwd(),
22
+ publicDir: publicDir,
26
23
  plugins: [
27
24
  // minifyTemplateLiterals(), // TODO: minification breaks hydration. The footprint difference is minimal after all
28
25
  ViteImageOptimizer({
@@ -52,7 +49,7 @@ export function createViteConfig(options = {}) {
52
49
  },
53
50
  resolve: {
54
51
  alias: {
55
- "@": path.resolve(process.cwd(), rootDir),
52
+ "@": srcDir,
56
53
  },
57
54
  },
58
55
  },
package/src/dev/index.js CHANGED
@@ -21,7 +21,9 @@ export async function dev(options = {}) {
21
21
  const config = await loadConfig(options)
22
22
 
23
23
  const mergedOptions = { ...config, ...options }
24
- const { rootDir = "src" } = mergedOptions
24
+ const { rootDir = "." } = mergedOptions
25
+
26
+ const pagesDir = path.join(rootDir, "src", "pages")
25
27
 
26
28
  console.log("🚀 Starting dev server...\n")
27
29
 
@@ -30,16 +32,8 @@ export async function dev(options = {}) {
30
32
  const viteServer = await createServer(viteConfig)
31
33
  const loader = (p) => viteServer.ssrLoadModule(p)
32
34
 
33
- // Get all pages once at startup
34
- const pages = await getPages(path.join(rootDir, "pages"), loader)
35
- console.log(`📄 Found ${pages.length} pages\n`)
36
-
37
- // Generate store config once for all pages
38
- const store = await generateStore(pages, mergedOptions, loader)
39
-
40
35
  // Use Vite's middleware first (handles HMR, static files, etc.)
41
36
  const connectServer = connect()
42
-
43
37
  connectServer.use(viteServer.middlewares)
44
38
 
45
39
  // Add SSR middleware
@@ -56,6 +50,9 @@ export async function dev(options = {}) {
56
50
  return next() // Let Vite serve it
57
51
  }
58
52
 
53
+ // Get all pages on each request (in dev mode, pages might be added/removed)
54
+ const pages = await getPages(pagesDir, loader)
55
+
59
56
  // Find matching page
60
57
  const page = pages.find((p) => matchRoute(p.path, url))
61
58
  if (!page) return next()
@@ -63,19 +60,30 @@ export async function dev(options = {}) {
63
60
  const module = await loader(page.filePath)
64
61
  page.module = module
65
62
 
63
+ // Generate store for THIS request (to pick up changes)
64
+ const store = await generateStore(pages, mergedOptions, loader)
65
+
66
66
  const entity = store._api.getEntity(page.moduleName)
67
67
  if (module.load) {
68
68
  await module.load(entity, page)
69
69
  }
70
+
71
+ // Generate and update the virtual app file BEFORE rendering
72
+ const app = generateApp(store, pages)
73
+ virtualFiles.set("/main.js", app)
74
+
75
+ // Invalidate the virtual module to ensure Vite picks up changes
76
+ const virtualModule = viteServer.moduleGraph.getModuleById("/main.js")
77
+ if (virtualModule) {
78
+ viteServer.moduleGraph.invalidateModule(virtualModule)
79
+ }
80
+
70
81
  const html = await renderPage(store, page, entity, {
71
82
  ...mergedOptions,
72
83
  wrap: true,
73
84
  isDev: true,
74
85
  })
75
86
 
76
- const app = generateApp(store, pages)
77
- virtualFiles.set("/main.js", app)
78
-
79
87
  res.setHeader("Content-Type", "text/html")
80
88
  res.end(html)
81
89
  } catch (error) {
@@ -15,24 +15,22 @@ import { markdownPlugin } from "../utils/markdown.js"
15
15
  * @returns {Object} The merged Vite configuration.
16
16
  */
17
17
  export function createViteConfig(options = {}) {
18
- const {
19
- rootDir = "src",
20
- publicDir = "public",
21
- vite = {},
22
- markdown = {},
23
- } = options
18
+ const { rootDir = ".", vite = {}, markdown = {} } = options
24
19
  const { port = 3000 } = vite.dev ?? {}
25
20
 
21
+ const srcDir = path.resolve(process.cwd(), rootDir, "src")
22
+ const publicDir = path.resolve(process.cwd(), rootDir, "public")
23
+
26
24
  return mergeConfig(
27
25
  {
28
26
  root: process.cwd(),
29
- publicDir: path.resolve(process.cwd(), rootDir, publicDir),
27
+ publicDir,
30
28
  server: { port, middlewareMode: true },
31
29
  appType: "custom",
32
30
  plugins: [virtualPlugin(), markdownPlugin(markdown)],
33
31
  resolve: {
34
32
  alias: {
35
- "@": path.resolve(process.cwd(), rootDir),
33
+ "@": srcDir,
36
34
  },
37
35
  },
38
36
  },
@@ -14,7 +14,9 @@ describe("createViteConfig", () => {
14
14
 
15
15
  it("should respect custom rootDir", () => {
16
16
  const config = createViteConfig({ rootDir: "app" })
17
- expect(config.resolve.alias["@"]).toBe(path.resolve(process.cwd(), "app"))
17
+ expect(config.resolve.alias["@"]).toBe(
18
+ path.resolve(process.cwd(), "app", "src"),
19
+ )
18
20
  })
19
21
 
20
22
  it("should merge custom vite config", () => {
@@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest"
7
7
  import { renderPage } from "."
8
8
 
9
9
  const ROOT_DIR = path.join(import.meta.dirname, "..", "__fixtures__")
10
- const PAGES_DIR = path.join(ROOT_DIR, "pages")
10
+ const PAGES_DIR = path.join(ROOT_DIR, "src", "pages")
11
11
 
12
12
  const DEFAULT_OPTIONS = { stripLitMarkers: true }
13
13
 
@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"
5
5
  import { getPages, getRoutes, matchRoute, resolvePage } from "./index.js"
6
6
 
7
7
  const ROOT_DIR = path.join(import.meta.dirname, "..", "__fixtures__")
8
- const PAGES_DIR = path.join(ROOT_DIR, "pages")
8
+ const PAGES_DIR = path.join(ROOT_DIR, "src", "pages")
9
9
 
10
10
  describe("router", () => {
11
11
  afterEach(() => {
@@ -5,7 +5,8 @@ import { describe, expect, it } from "vitest"
5
5
  import { generateStore } from "../store"
6
6
  import { generateApp } from "./app"
7
7
 
8
- const ROOT_DIR = path.join(__dirname, "..", "__fixtures__")
8
+ const ROOT_DIR = path.join(import.meta.dirname, "..", "__fixtures__")
9
+ const PAGES_DIR = path.join(ROOT_DIR, "src", "pages")
9
10
 
10
11
  describe("generateApp", () => {
11
12
  it("should generate the app script for a static page", async () => {
@@ -13,7 +14,7 @@ describe("generateApp", () => {
13
14
  pattern: "/",
14
15
  path: "/",
15
16
  modulePath: "index.js",
16
- filePath: path.join(ROOT_DIR, "pages", "index.js"),
17
+ filePath: PAGES_DIR,
17
18
  }
18
19
  const store = await generateStore([page], { rootDir: ROOT_DIR })
19
20
 
@@ -27,7 +28,7 @@ describe("generateApp", () => {
27
28
  pattern: "/about",
28
29
  path: "/about",
29
30
  modulePath: "about.js",
30
- filePath: path.join(ROOT_DIR, "pages", "about.js"),
31
+ filePath: path.join(PAGES_DIR, "about.js"),
31
32
  }
32
33
  const store = await generateStore([page], { rootDir: ROOT_DIR })
33
34
 
@@ -41,7 +42,7 @@ describe("generateApp", () => {
41
42
  pattern: "/blog",
42
43
  path: "/blog",
43
44
  modulePath: "blog.js",
44
- filePath: path.join(ROOT_DIR, "pages", "blog.js"),
45
+ filePath: path.join(PAGES_DIR, "blog.js"),
45
46
  }
46
47
  const store = await generateStore([page], { rootDir: ROOT_DIR })
47
48
 
@@ -55,7 +56,7 @@ describe("generateApp", () => {
55
56
  pattern: "/posts/:slug",
56
57
  path: "/posts/my-first-post",
57
58
  modulePath: "post.js",
58
- filePath: path.join(ROOT_DIR, "pages", "posts", "_slug.js"),
59
+ filePath: path.join(PAGES_DIR, "posts", "_slug.js"),
59
60
  }
60
61
  const store = await generateStore([page], { rootDir: ROOT_DIR })
61
62
 
@@ -17,7 +17,9 @@ import { getModuleName } from "../utils/module.js"
17
17
  * @returns {Promise<Object>} The initialized store instance.
18
18
  */
19
19
  export async function generateStore(pages = [], options = {}, loader) {
20
- const { rootDir = "src" } = options
20
+ const { rootDir = "." } = options
21
+ const srcDir = path.join(rootDir, "src")
22
+
21
23
  const load = loader || ((p) => import(pathToFileURL(p)))
22
24
 
23
25
  const types = {}
@@ -32,7 +34,7 @@ export async function generateStore(pages = [], options = {}, loader) {
32
34
 
33
35
  for (const ext of extensions) {
34
36
  try {
35
- const module = await load(path.join(rootDir, "store", `entities.${ext}`))
37
+ const module = await load(path.join(srcDir, "store", `entities.${ext}`))
36
38
  entities = module.entities
37
39
  break
38
40
  } catch {
@@ -5,11 +5,12 @@ import { describe, expect, it, vi } from "vitest"
5
5
  import { generateStore } from "."
6
6
 
7
7
  const ROOT_DIR = path.join(import.meta.dirname, "..", "__fixtures__")
8
+ const PAGES_DIR = path.join(ROOT_DIR, "src", "pages")
8
9
 
9
10
  describe("generateStore", () => {
10
11
  it("should generate the proper types and entities from a static page", async () => {
11
12
  const page = {
12
- filePath: path.join(ROOT_DIR, "pages", "index.js"),
13
+ filePath: path.join(PAGES_DIR, "index.js"),
13
14
  }
14
15
 
15
16
  const store = await generateStore([page], { rootDir: ROOT_DIR })
@@ -20,7 +21,7 @@ describe("generateStore", () => {
20
21
 
21
22
  it("should generate the proper types and entities from a page with an entity", async () => {
22
23
  const page = {
23
- filePath: path.join(ROOT_DIR, "pages", "about.js"),
24
+ filePath: path.join(PAGES_DIR, "about.js"),
24
25
  }
25
26
 
26
27
  const store = await generateStore([page], { rootDir: ROOT_DIR })
@@ -31,7 +32,7 @@ describe("generateStore", () => {
31
32
 
32
33
  it("should generate the proper types and entities from a page that has metadata", async () => {
33
34
  const page = {
34
- filePath: path.join(ROOT_DIR, "pages", "blog.js"),
35
+ filePath: path.join(PAGES_DIR, "blog.js"),
35
36
  }
36
37
 
37
38
  const store = await generateStore([page], { rootDir: ROOT_DIR })
@@ -42,7 +43,7 @@ describe("generateStore", () => {
42
43
 
43
44
  it("should handle missing entities.js gracefully", async () => {
44
45
  const page = {
45
- filePath: path.join(ROOT_DIR, "pages", "index.js"),
46
+ filePath: path.join(PAGES_DIR, "index.js"),
46
47
  }
47
48
 
48
49
  // Point to a directory that doesn't contain entities.js
@@ -64,8 +65,8 @@ describe("generateStore", () => {
64
65
  throw new Error("MODULE_NOT_FOUND")
65
66
  })
66
67
 
67
- const page = { filePath: path.join(ROOT_DIR, "pages", "index.js") }
68
- await generateStore([page], { rootDir: "src" }, loader)
68
+ const page = { filePath: path.join(PAGES_DIR, "index.js") }
69
+ await generateStore([page], { rootDir: "." }, loader)
69
70
 
70
71
  expect(loader).toHaveBeenCalledWith(page.filePath)
71
72
  expect(loader).toHaveBeenCalledWith(
@@ -2,10 +2,11 @@ import path from "node:path"
2
2
  import { pathToFileURL } from "node:url"
3
3
 
4
4
  export async function loadConfig(options) {
5
- const { rootDir = "src", configPath = "site.config.js" } = options
5
+ const { rootDir = ".", configPath = "site.config.js" } = options
6
+ const srcDir = path.join(rootDir, "src")
6
7
 
7
8
  try {
8
- const config = await import(pathToFileURL(path.join(rootDir, configPath)))
9
+ const config = await import(pathToFileURL(path.join(srcDir, configPath)))
9
10
  return config.default || config
10
11
  } catch (error) {
11
12
  if (error.code === "MODULE_NOT_FOUND") {