@inglorious/ssx 0.4.1 → 1.1.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.
@@ -0,0 +1,51 @@
1
+ import path from "node:path"
2
+
3
+ import { mergeConfig } from "vite"
4
+
5
+ // import { minifyTemplateLiterals } from "rollup-plugin-minify-template-literals"
6
+
7
+ /**
8
+ * Generate Vite config for building the client bundle
9
+ */
10
+ export function createViteConfig(options = {}) {
11
+ const {
12
+ rootDir = "src",
13
+ outDir = "dist",
14
+ publicDir = "public",
15
+ vite = {},
16
+ } = options
17
+
18
+ return mergeConfig(
19
+ {
20
+ root: rootDir,
21
+ publicDir: path.resolve(process.cwd(), rootDir, publicDir),
22
+ // plugins: [minifyTemplateLiterals()], // TODO: minification breaks hydration. The footprint difference is minimal after all
23
+ build: {
24
+ outDir,
25
+ emptyOutDir: false, // Don't delete HTML files we already generated
26
+ rollupOptions: {
27
+ input: {
28
+ main: path.resolve(outDir, "main.js"),
29
+ },
30
+ output: {
31
+ entryFileNames: "[name].js",
32
+ chunkFileNames: "[name].[hash].js",
33
+ assetFileNames: "[name].[ext]",
34
+
35
+ manualChunks(id) {
36
+ if (id.includes("node_modules")) {
37
+ return "lib"
38
+ }
39
+ },
40
+ },
41
+ },
42
+ },
43
+ resolve: {
44
+ alias: {
45
+ "@": path.resolve(process.cwd(), rootDir),
46
+ },
47
+ },
48
+ },
49
+ vite,
50
+ )
51
+ }
package/src/config.js ADDED
@@ -0,0 +1,16 @@
1
+ import path from "node:path"
2
+ import { pathToFileURL } from "node:url"
3
+
4
+ export async function loadConfig(options) {
5
+ const { rootDir = "src", configPath = "site.config.js" } = options
6
+
7
+ try {
8
+ const config = await import(pathToFileURL(path.join(rootDir, configPath)))
9
+ return config.default || config
10
+ } catch (error) {
11
+ if (error.code === "MODULE_NOT_FOUND") {
12
+ return {} // Default config
13
+ }
14
+ throw error
15
+ }
16
+ }
@@ -3,13 +3,18 @@ import path from "node:path"
3
3
  import connect from "connect"
4
4
  import { createServer } from "vite"
5
5
 
6
- import { renderPage } from "./render.js"
7
- import { getPages } from "./router.js"
8
- import { generateApp } from "./scripts/app.js"
9
- import { generateStore } from "./store.js"
6
+ import { loadConfig } from "../config.js"
7
+ import { renderPage } from "../render/index.js"
8
+ import { getPages } from "../router/index.js"
9
+ import { generateApp } from "../scripts/app.js"
10
+ import { generateStore } from "../store.js"
11
+ import { createViteConfig, virtualFiles } from "./vite-config.js"
10
12
 
11
13
  export async function dev(options = {}) {
12
- const { rootDir = "src", port = 3000, renderOptions = {} } = options
14
+ const config = await loadConfig(options)
15
+
16
+ const mergedOptions = { ...config, ...options }
17
+ const { rootDir = "src" } = mergedOptions
13
18
 
14
19
  console.log("🚀 Starting dev server...\n")
15
20
 
@@ -18,20 +23,11 @@ export async function dev(options = {}) {
18
23
  console.log(`📄 Found ${pages.length} pages\n`)
19
24
 
20
25
  // Generate store config once for all pages
21
- const store = await generateStore(pages, options)
26
+ const store = await generateStore(pages, mergedOptions)
22
27
 
23
28
  // Create Vite dev server
24
- const viteServer = await createServer({
25
- root: process.cwd(),
26
- server: { port, middlewareMode: true },
27
- appType: "custom",
28
- plugins: [virtualPlugin()],
29
- resolve: {
30
- alias: {
31
- "@": path.resolve(process.cwd(), rootDir),
32
- },
33
- },
34
- })
29
+ const viteConfig = createViteConfig(mergedOptions)
30
+ const viteServer = await createServer(viteConfig)
35
31
 
36
32
  // Use Vite's middleware first (handles HMR, static files, etc.)
37
33
  const connectServer = connect()
@@ -43,14 +39,30 @@ export async function dev(options = {}) {
43
39
  const [url] = req.url.split("?")
44
40
 
45
41
  try {
42
+ // Skip special routes, static files, AND public assets
43
+ if (
44
+ url.startsWith("/@") ||
45
+ url.includes(".") || // Vite handles static files
46
+ url === "/favicon.ico"
47
+ ) {
48
+ return next() // Let Vite serve it
49
+ }
50
+
46
51
  // Find matching page
47
52
  const page = pages.find((p) => matchRoute(p.path, url))
48
53
  if (!page) return next()
49
54
 
50
55
  const module = await viteServer.ssrLoadModule(page.filePath)
51
- const html = await renderPage(store, page, module, {
52
- ...renderOptions,
56
+ page.module = module
57
+
58
+ const entity = store._api.getEntity(page.moduleName)
59
+ if (module.load) {
60
+ await module.load(entity, page)
61
+ }
62
+ const html = await renderPage(store, page, entity, {
63
+ ...mergedOptions,
53
64
  wrap: true,
65
+ isDev: true,
54
66
  })
55
67
 
56
68
  const app = generateApp(store, pages)
@@ -64,6 +76,7 @@ export async function dev(options = {}) {
64
76
  }
65
77
  })
66
78
 
79
+ const { port = 3000 } = viteConfig.server ?? {}
67
80
  const server = connectServer.listen(port)
68
81
 
69
82
  console.log(`\n✨ Dev server running at http://localhost:${port}\n`)
@@ -93,19 +106,3 @@ function matchRoute(pattern, url) {
93
106
  return part === urlParts[i]
94
107
  })
95
108
  }
96
-
97
- const virtualFiles = new Map()
98
-
99
- function virtualPlugin() {
100
- return {
101
- name: "ssx-virtual-files",
102
-
103
- resolveId(id) {
104
- if (virtualFiles.has(id)) return id
105
- },
106
-
107
- load(id) {
108
- if (virtualFiles.has(id)) return virtualFiles.get(id)
109
- },
110
- }
111
- }
@@ -0,0 +1,40 @@
1
+ import path from "node:path"
2
+
3
+ import { mergeConfig } from "vite"
4
+
5
+ export function createViteConfig(options = {}) {
6
+ const { rootDir = "src", publicDir = "public", vite = {} } = options
7
+ const { port = 3000 } = vite.dev ?? {}
8
+
9
+ return mergeConfig(
10
+ {
11
+ root: process.cwd(),
12
+ publicDir: path.resolve(process.cwd(), rootDir, publicDir),
13
+ server: { port, middlewareMode: true },
14
+ appType: "custom",
15
+ plugins: [virtualPlugin()],
16
+ resolve: {
17
+ alias: {
18
+ "@": path.resolve(process.cwd(), rootDir),
19
+ },
20
+ },
21
+ },
22
+ vite,
23
+ )
24
+ }
25
+
26
+ export const virtualFiles = new Map()
27
+
28
+ function virtualPlugin() {
29
+ return {
30
+ name: "ssx-virtual-files",
31
+
32
+ resolveId(id) {
33
+ if (virtualFiles.has(id)) return id
34
+ },
35
+
36
+ load(id) {
37
+ if (virtualFiles.has(id)) return virtualFiles.get(id)
38
+ },
39
+ }
40
+ }
package/src/module.js CHANGED
@@ -1,8 +1,5 @@
1
- const RESERVED = ["title", "meta", "scripts", "styles", "load"]
2
-
3
1
  export function getModuleName(pageModule) {
4
2
  const name = Object.keys(pageModule).find((key) => {
5
- if (RESERVED.includes(key)) return false
6
3
  const value = pageModule[key]
7
4
  return (
8
5
  value && typeof value === "object" && typeof value.render === "function"
@@ -0,0 +1,8 @@
1
+ export function createGetPageOption(store, module, entity) {
2
+ let { metadata = {} } = module
3
+ if (typeof metadata === "function") {
4
+ metadata = metadata(entity, store._api)
5
+ }
6
+
7
+ return (name, defaults) => metadata[name] ?? defaults[name]
8
+ }
@@ -2,6 +2,8 @@ import { html } from "@inglorious/web"
2
2
  import { render as ssrRender } from "@lit-labs/ssr"
3
3
  import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
4
4
 
5
+ import { layout as defaultLayout } from "./layout.js"
6
+
5
7
  export async function toHTML(store, renderFn, options = {}) {
6
8
  const api = { ...store._api }
7
9
  api.render = createRender(api)
@@ -17,7 +19,10 @@ export async function toHTML(store, renderFn, options = {}) {
17
19
  ? stripLitMarkers(resultString)
18
20
  : resultString
19
21
 
20
- return options.wrap ? wrapHTML(finalHTML, options) : finalHTML
22
+ if (!options.wrap) return finalHTML
23
+
24
+ const layout = options.layout ?? defaultLayout
25
+ return options.wrap ? layout(finalHTML, options) : finalHTML
21
26
  }
22
27
 
23
28
  function stripLitMarkers(html) {
@@ -26,28 +31,7 @@ function stripLitMarkers(html) {
26
31
  .replace(/<!--\s*-->/g, "") // Empty comments
27
32
  }
28
33
 
29
- function wrapHTML(body, options) {
30
- const { dev, title = "", meta = {}, styles = [], scripts = [] } = options
31
-
32
- return `<!DOCTYPE html>
33
- <html>
34
- <head>
35
- <meta charset="UTF-8">
36
- <title>${title}</title>
37
- ${Object.entries(meta)
38
- .map(([name, content]) => `<meta name="${name}" content="${content}">`)
39
- .join("\n")}
40
- ${styles.map((href) => `<link rel="stylesheet" href="${href}">`).join("\n")}
41
- </head>
42
- <body>
43
- <div id="root">${body}</div>
44
-
45
- ${dev ? `<script type="module" src="/@vite/client"></script>` : ``}<script type="module" src="/main.js"></script>
46
- ${scripts.map((src) => `<script type="module" src="${src}"></script>`).join("\n")}
47
- </body>
48
- </html>`
49
- }
50
-
34
+ // TODO: this was copied from @inglorious/web, maybe expose it?
51
35
  function createRender(api) {
52
36
  return function (id, options = {}) {
53
37
  const entity = api.getEntity(id)
@@ -0,0 +1,43 @@
1
+ import { createGetPageOption } from "../page-options.js"
2
+ import { toHTML } from "./html.js"
3
+
4
+ const DEFAULT_OPTIONS = {
5
+ lang: "en",
6
+ charset: "UTF-8",
7
+ title: "",
8
+ meta: {},
9
+ styles: [],
10
+ head: "",
11
+ scripts: [],
12
+ }
13
+
14
+ export async function renderPage(store, page, entity, options = {}) {
15
+ const { moduleName, module } = page
16
+
17
+ const getPageOption = createGetPageOption(store, module, entity)
18
+
19
+ const lang = getPageOption("lang", DEFAULT_OPTIONS) ?? options.lang
20
+ const charset = getPageOption("charset", DEFAULT_OPTIONS) ?? options.charset
21
+ const title = getPageOption("title", DEFAULT_OPTIONS) ?? options.title
22
+ const meta = { ...options.meta, ...getPageOption("meta", DEFAULT_OPTIONS) }
23
+ const styles = [
24
+ ...(options.styles ?? []),
25
+ ...getPageOption("styles", DEFAULT_OPTIONS),
26
+ ]
27
+ const head = getPageOption("head", DEFAULT_OPTIONS) ?? options.head
28
+ const scripts = [
29
+ ...(options.scripts ?? []),
30
+ ...getPageOption("scripts", DEFAULT_OPTIONS),
31
+ ]
32
+
33
+ return toHTML(store, (api) => api.render(moduleName, { allowType: true }), {
34
+ ...options,
35
+ lang,
36
+ charset,
37
+ title,
38
+ meta,
39
+ styles,
40
+ head,
41
+ scripts,
42
+ })
43
+ }
@@ -0,0 +1,37 @@
1
+ export function layout(body, options) {
2
+ const {
3
+ lang = "en",
4
+ charset = "UTF-8",
5
+ title = "",
6
+ meta = {},
7
+ styles = [],
8
+ head = "",
9
+ scripts = [],
10
+ isDev,
11
+ } = options
12
+
13
+ return `<!DOCTYPE html>
14
+ <html lang="${lang}">
15
+ <head>
16
+ <meta charset="${charset}" />
17
+ <title>${title}</title>
18
+ ${Object.entries(meta)
19
+ .map(
20
+ ([name, content]) => `<meta name="${name}" content="${content}">`,
21
+ )
22
+ .join("\n")}
23
+ ${styles
24
+ .map((href) => `<link rel="stylesheet" href="${href}">`)
25
+ .join("\n")}
26
+ ${head}
27
+ </head>
28
+ <body>
29
+ <div id="root">${body}</div>
30
+ ${isDev ? `<script type="module" src="/@vite/client"></script>` : ``}
31
+ <script type="module" src="/main.js"></script>
32
+ ${scripts
33
+ .map((src) => `<script type="module" src="${src}"></script>`)
34
+ .join("\n")}
35
+ </body>
36
+ </html>`
37
+ }
@@ -0,0 +1,111 @@
1
+ import path from "node:path"
2
+
3
+ import { createStore } from "@inglorious/web"
4
+ import { expect, it } from "vitest"
5
+
6
+ import { renderPage } from "."
7
+
8
+ const ROOT_DIR = path.join(__dirname, "..", "__fixtures__")
9
+ const PAGES_DIR = path.join(ROOT_DIR, "pages")
10
+
11
+ const DEFAULT_OPTIONS = { stripLitMarkers: true }
12
+
13
+ it("should render a static page fragment", async () => {
14
+ const module = await import(path.resolve(path.join(PAGES_DIR, "index.js")))
15
+ const page = { path: "/", moduleName: "index", module }
16
+
17
+ const store = createStore({
18
+ types: { index: module.index },
19
+ updateMode: "manual",
20
+ })
21
+
22
+ const html = await renderPage(store, page, undefined, DEFAULT_OPTIONS)
23
+
24
+ expect(html).toMatchSnapshot()
25
+ })
26
+
27
+ it("should render a page with entity", async () => {
28
+ const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
29
+ const page = { path: "/about", moduleName: "about", module }
30
+ const entity = { type: "about", name: "Us" }
31
+
32
+ const store = createStore({
33
+ types: { about: module.about },
34
+ entities: { about: entity },
35
+ updateMode: "manual",
36
+ })
37
+
38
+ const html = await renderPage(store, page, entity, DEFAULT_OPTIONS)
39
+
40
+ expect(html).toMatchSnapshot()
41
+ })
42
+
43
+ it("should render a page with metadata", async () => {
44
+ const module = await import(path.resolve(path.join(PAGES_DIR, "about.js")))
45
+ const page = { path: "/about", moduleName: "about", module }
46
+ const entity = { type: "about", name: "Us" }
47
+
48
+ const store = createStore({
49
+ types: { about: module.about },
50
+ entities: { about: entity },
51
+ updateMode: "manual",
52
+ })
53
+
54
+ const html = await renderPage(store, page, module, {
55
+ ...DEFAULT_OPTIONS,
56
+ wrap: true,
57
+ })
58
+
59
+ expect(html).toMatchSnapshot()
60
+ })
61
+
62
+ it("should render a page with pre-fetched data", async () => {
63
+ const module = await import(path.resolve(path.join(PAGES_DIR, "blog.js")))
64
+ const page = { path: "/blog", moduleName: "blog", module }
65
+ const entity = {
66
+ type: "blog",
67
+ name: "Antony",
68
+ posts: [
69
+ { id: 1, title: "First Post" },
70
+ { id: 2, title: "Second Post" },
71
+ { id: 3, title: "Third Post" },
72
+ ],
73
+ }
74
+
75
+ const store = createStore({
76
+ types: { blog: module.blog },
77
+ entities: { blog: entity },
78
+ updateMode: "manual",
79
+ })
80
+
81
+ const html = await renderPage(store, page, module, DEFAULT_OPTIONS)
82
+
83
+ expect(html).toMatchSnapshot()
84
+ })
85
+
86
+ it("should render a dynamic page", async () => {
87
+ const module = await import(
88
+ path.resolve(path.join(PAGES_DIR, "posts/_slug.js"))
89
+ )
90
+ const page = { path: "/posts/1", moduleName: "post", module }
91
+ const entity = {
92
+ type: "blog",
93
+ name: "Antony",
94
+ post: {
95
+ id: 1,
96
+ title: "First Post",
97
+ date: "2026-01-04",
98
+ body: "Hello world!",
99
+ },
100
+ }
101
+
102
+ const store = createStore({
103
+ types: { blog: module.post },
104
+ entities: { post: entity },
105
+ updateMode: "manual",
106
+ })
107
+
108
+ const html = await renderPage(store, page, module, DEFAULT_OPTIONS)
109
+
110
+ expect(html).toMatchSnapshot()
111
+ })
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url"
3
3
 
4
4
  import { glob } from "glob"
5
5
 
6
- import { getModuleName } from "./module.js"
6
+ import { getModuleName } from "../module.js"
7
7
 
8
8
  const NEXT_MATCH = 1
9
9
 
@@ -13,7 +13,6 @@ const SCORE_MULTIPLIER = 0.1
13
13
 
14
14
  /**
15
15
  * Get all static paths for SSG build.
16
- * This calls getStaticPaths() on dynamic route pages.
17
16
  */
18
17
  export async function getPages(pagesDir = "pages") {
19
18
  const routes = await getRoutes(pagesDir)
@@ -24,28 +23,30 @@ export async function getPages(pagesDir = "pages") {
24
23
  const moduleName = getModuleName(module)
25
24
 
26
25
  if (isDynamic(route.pattern)) {
27
- // Dynamic route - call getStaticPaths if it exists
28
- if (typeof module.getStaticPaths === "function") {
29
- const paths = await module.getStaticPaths()
26
+ let { staticPaths = [] } = module
27
+ if (typeof staticPaths === "function") {
28
+ staticPaths = await staticPaths()
29
+ }
30
30
 
31
- for (const pathOrObject of paths) {
32
- const urlPath =
31
+ if (staticPaths.length) {
32
+ for (const pathOrObject of staticPaths) {
33
+ const path =
33
34
  typeof pathOrObject === "string" ? pathOrObject : pathOrObject.path
34
35
 
35
- const params = extractParams(route, urlPath)
36
+ const params = extractParams(route, path)
36
37
 
37
38
  pages.push({
38
39
  pattern: route.pattern,
39
- path: urlPath,
40
+ path,
41
+ params,
42
+ moduleName,
40
43
  modulePath: route.modulePath,
41
44
  filePath: route.filePath,
42
- moduleName,
43
- params,
44
45
  })
45
46
  }
46
47
  } else {
47
48
  console.warn(
48
- `Dynamic route ${route.filePath} has no getStaticPaths export. ` +
49
+ `Dynamic route ${route.filePath} has no staticPaths export. ` +
49
50
  `It will be skipped during SSG.`,
50
51
  )
51
52
  }
@@ -54,10 +55,11 @@ export async function getPages(pagesDir = "pages") {
54
55
  pages.push({
55
56
  pattern: route.pattern,
56
57
  path: route.pattern || "/",
58
+ params: {},
59
+ module,
60
+ moduleName,
57
61
  modulePath: route.modulePath,
58
62
  filePath: route.filePath,
59
- moduleName,
60
- params: {},
61
63
  })
62
64
  }
63
65
  }
@@ -2,9 +2,9 @@ import path from "node:path"
2
2
 
3
3
  import { afterEach, describe, expect, it, vi } from "vitest"
4
4
 
5
- import { getPages, getRoutes, resolvePage } from "./router.js"
5
+ import { getPages, getRoutes, resolvePage } from "./index.js"
6
6
 
7
- const ROOT_DIR = path.join(__dirname, "__fixtures__")
7
+ const ROOT_DIR = path.join(__dirname, "..", "__fixtures__")
8
8
  const PAGES_DIR = path.join(ROOT_DIR, "pages")
9
9
 
10
10
  describe("router", () => {
@@ -25,8 +25,8 @@ describe("router", () => {
25
25
 
26
26
  const patterns = routes.map((r) => r.pattern)
27
27
 
28
- expect(patterns).toContain("/posts/:id")
29
- expect(patterns).toContain("/blog/:slug")
28
+ expect(patterns).toContain("/posts/:slug")
29
+ expect(patterns).toContain("/blog")
30
30
  expect(patterns).toContain("/about")
31
31
  expect(patterns).toContain("/")
32
32
  expect(patterns).toContain("/api/*")
@@ -39,7 +39,7 @@ describe("router", () => {
39
39
  // Root usually comes after specific paths but before catch-all if it was a catch-all root,
40
40
  // but here / is static.
41
41
  // Let's just check that we found them.
42
- expect(routes).toHaveLength(6)
42
+ expect(routes).toHaveLength(5)
43
43
  })
44
44
  })
45
45
 
@@ -59,9 +59,9 @@ describe("router", () => {
59
59
  })
60
60
 
61
61
  it("should resolve dynamic page with params", async () => {
62
- const page = await resolvePage("/blog/hello-world", PAGES_DIR)
62
+ const page = await resolvePage("/posts/hello-world", PAGES_DIR)
63
63
  expect(page).not.toBeNull()
64
- expect(page.filePath).toContain("blog")
64
+ expect(page.filePath).toContain("posts")
65
65
  expect(page.params).toEqual({ slug: "hello-world" })
66
66
  })
67
67
 
@@ -93,12 +93,12 @@ describe("router", () => {
93
93
 
94
94
  expect(pages).toMatchSnapshot()
95
95
 
96
- // Dynamic route without getStaticPaths should be skipped (and warn)
96
+ // Dynamic route without staticPaths should be skipped (and warn)
97
97
  const blogPage = pages.find((p) => p.path.includes("/blog/"))
98
98
  expect(blogPage).toBeUndefined()
99
99
 
100
100
  expect(consoleSpy).toHaveBeenCalled()
101
- expect(consoleSpy.mock.calls[1][0]).toContain("has no getStaticPaths")
101
+ expect(consoleSpy.mock.calls[1][0]).toContain("has no staticPaths")
102
102
  })
103
103
  })
104
104
  })
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-unused-vars */
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.
@@ -18,7 +16,15 @@ export function generateApp(store, pages) {
18
16
  return `import { createDevtools, createStore, mount } from "@inglorious/web"
19
17
  import { getRoute, router, setRoutes } from "@inglorious/web/router"
20
18
 
21
- const pages = ${JSON.stringify(pages.map(({ filePath, ...page }) => page))}
19
+ const pages = ${JSON.stringify(
20
+ pages.map(({ pattern, path, moduleName }) => ({
21
+ pattern,
22
+ path,
23
+ moduleName,
24
+ })),
25
+ null,
26
+ 2,
27
+ )}
22
28
  const path = window.location.pathname + window.location.search + window.location.hash
23
29
  const page = pages.find((page) => page.path === path)
24
30
 
@@ -9,6 +9,7 @@ const ROOT_DIR = path.join(__dirname, "..", "__fixtures__")
9
9
 
10
10
  it("should generate the app script for a static page", async () => {
11
11
  const page = {
12
+ pattern: "/",
12
13
  path: "/",
13
14
  modulePath: "index.js",
14
15
  filePath: path.join(ROOT_DIR, "pages", "index.js"),
@@ -22,6 +23,7 @@ it("should generate the app script for a static page", async () => {
22
23
 
23
24
  it("should generate the app script for a page with an entity", async () => {
24
25
  const page = {
26
+ pattern: "/about",
25
27
  path: "/about",
26
28
  modulePath: "about.js",
27
29
  filePath: path.join(ROOT_DIR, "pages", "about.js"),
@@ -35,9 +37,24 @@ it("should generate the app script for a page with an entity", async () => {
35
37
 
36
38
  it("should generate the app script for a page that has metadata", async () => {
37
39
  const page = {
38
- path: "/posts",
39
- modulePath: "posts.js",
40
- filePath: path.join(ROOT_DIR, "pages", "posts.js"),
40
+ pattern: "/blog",
41
+ path: "/blog",
42
+ modulePath: "blog.js",
43
+ filePath: path.join(ROOT_DIR, "pages", "blog.js"),
44
+ }
45
+ const store = await generateStore([page], { rootDir: ROOT_DIR })
46
+
47
+ const app = generateApp(store, [page])
48
+
49
+ expect(app).toMatchSnapshot()
50
+ })
51
+
52
+ it("should generate the app script for a dynamic page", async () => {
53
+ const page = {
54
+ pattern: "/posts/:slug",
55
+ path: "/posts/my-first-post",
56
+ modulePath: "post.js",
57
+ filePath: path.join(ROOT_DIR, "pages", "posts", "_slug.js"),
41
58
  }
42
59
  const store = await generateStore([page], { rootDir: ROOT_DIR })
43
60