@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.
- package/README.md +444 -662
- package/bin/ssx.js +17 -21
- package/package.json +2 -7
- package/src/{build.test.js → build/build.test.js} +1 -1
- package/src/build/index.js +161 -0
- package/src/build/manifest.js +112 -0
- package/src/build/metadata.js +53 -0
- package/src/build/pages.js +40 -0
- package/src/build/public.js +34 -0
- package/src/build/rss.js +102 -0
- package/src/build/sitemap.js +57 -0
- package/src/build/vite-config.js +51 -0
- package/src/config.js +16 -0
- package/src/{dev.js → dev/index.js} +32 -35
- package/src/dev/vite-config.js +40 -0
- package/src/module.js +0 -3
- package/src/page-options.js +8 -0
- package/src/{html.js → render/html.js} +7 -23
- package/src/render/index.js +43 -0
- package/src/render/layout.js +37 -0
- package/src/render/render.test.js +111 -0
- package/src/{router.js → router/index.js} +16 -14
- package/src/{router.test.js → router/router.test.js} +9 -9
- package/src/scripts/app.js +9 -3
- package/src/scripts/app.test.js +20 -3
- package/src/store.js +9 -3
- package/src/store.test.js +2 -2
- package/src/build.js +0 -96
- package/src/random.js +0 -30
- package/src/render.js +0 -48
- package/src/render.test.js +0 -72
- package/src/vite-config.js +0 -40
- /package/src/{html.test.js → render/html.test.js} +0 -0
|
@@ -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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
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,
|
|
26
|
+
const store = await generateStore(pages, mergedOptions)
|
|
22
27
|
|
|
23
28
|
// Create Vite dev server
|
|
24
|
-
const
|
|
25
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
28
|
-
if (typeof
|
|
29
|
-
|
|
26
|
+
let { staticPaths = [] } = module
|
|
27
|
+
if (typeof staticPaths === "function") {
|
|
28
|
+
staticPaths = await staticPaths()
|
|
29
|
+
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
36
|
+
const params = extractParams(route, path)
|
|
36
37
|
|
|
37
38
|
pages.push({
|
|
38
39
|
pattern: route.pattern,
|
|
39
|
-
path
|
|
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
|
|
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 "./
|
|
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/:
|
|
29
|
-
expect(patterns).toContain("/blog
|
|
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(
|
|
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("/
|
|
62
|
+
const page = await resolvePage("/posts/hello-world", PAGES_DIR)
|
|
63
63
|
expect(page).not.toBeNull()
|
|
64
|
-
expect(page.filePath).toContain("
|
|
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
|
|
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
|
|
101
|
+
expect(consoleSpy.mock.calls[1][0]).toContain("has no staticPaths")
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
104
|
})
|
package/src/scripts/app.js
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/scripts/app.test.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|