@kaathewise/ssg 0.3.1 → 0.4.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/package.json +2 -1
- package/src/build.ts +8 -10
- package/src/config.ts +33 -0
- package/src/index.ts +2 -1
- package/src/run.ts +45 -0
- package/src/server.ts +68 -67
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaathewise/ssg",
|
|
3
3
|
"description": "Bun & JSX static site generator",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "Andrej Kolčin",
|
|
7
7
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"check": "biome check && tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"main": "src/index.ts",
|
|
12
|
+
"bin": "./src/run.ts",
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@biomejs/biome": "^2.2.3",
|
|
14
15
|
"@types/bun": "^1.2.21",
|
package/src/build.ts
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises"
|
|
2
2
|
import * as path from "node:path"
|
|
3
3
|
import { Glob, write } from "bun"
|
|
4
|
+
import type { Config } from "./config"
|
|
4
5
|
import { type Page, renderAll } from "./render"
|
|
5
6
|
|
|
6
7
|
const glob = new Glob("**/*.{js,jsx,ts,tsx}")
|
|
7
8
|
|
|
8
|
-
export async function build(
|
|
9
|
-
pagesDir = path.join(process.cwd(), pagesDir)
|
|
10
|
-
outDir = path.join(process.cwd(), outDir)
|
|
11
|
-
|
|
9
|
+
export async function build(config: Config) {
|
|
12
10
|
const pages: Page[] = []
|
|
13
|
-
for await (const relPath of glob.scan(pagesDir)) {
|
|
14
|
-
const modulePath = path.join(pagesDir, relPath)
|
|
15
|
-
const p = await renderAll(modulePath, pagesDir)
|
|
11
|
+
for await (const relPath of glob.scan(config.pagesDir)) {
|
|
12
|
+
const modulePath = path.join(config.pagesDir, relPath)
|
|
13
|
+
const p = await renderAll(modulePath, config.pagesDir)
|
|
16
14
|
pages.push(...p)
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
await fs.rm(
|
|
20
|
-
await fs.mkdir(
|
|
17
|
+
await fs.rm(config.outputDir, { recursive: true, force: true })
|
|
18
|
+
await fs.mkdir(config.outputDir)
|
|
21
19
|
|
|
22
20
|
for (const page of pages) {
|
|
23
|
-
const destPath = path.join(
|
|
21
|
+
const destPath = path.join(config.outputDir, page.path)
|
|
24
22
|
const dir = path.dirname(destPath)
|
|
25
23
|
if (!(await fs.exists(dir))) {
|
|
26
24
|
await fs.mkdir(path.dirname(destPath), {
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from "node:path"
|
|
2
|
+
|
|
3
|
+
const OUTPUT_DIR = "dist/"
|
|
4
|
+
const DEFAULT_PORT = 3001
|
|
5
|
+
|
|
6
|
+
export type Config = {
|
|
7
|
+
pagesDir: string
|
|
8
|
+
sourceDir: string
|
|
9
|
+
assetDir?: string
|
|
10
|
+
outputDir: string
|
|
11
|
+
port: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function defineConfig(options: {
|
|
15
|
+
pagesDir: string
|
|
16
|
+
sourceDir: string
|
|
17
|
+
assetDir?: string
|
|
18
|
+
outputDir?: string
|
|
19
|
+
port?: number
|
|
20
|
+
}) {
|
|
21
|
+
const config = {
|
|
22
|
+
pagesDir: path.resolve(options.pagesDir),
|
|
23
|
+
sourceDir: path.resolve(options.sourceDir),
|
|
24
|
+
outputDir: path.resolve(options.outputDir ?? OUTPUT_DIR),
|
|
25
|
+
port: options.port ?? DEFAULT_PORT,
|
|
26
|
+
} as Config
|
|
27
|
+
|
|
28
|
+
if (options.assetDir) {
|
|
29
|
+
config.assetDir = path.resolve(options.assetDir)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return config
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
package/src/run.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs/promises"
|
|
4
|
+
import * as path from "node:path"
|
|
5
|
+
import { build, type Config, serve } from "./index"
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = "ssg.config.ts"
|
|
8
|
+
const HELP = `Usage: ssg <command>
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
build: build the website
|
|
12
|
+
dev: start a hot-reloading development server
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2)
|
|
16
|
+
|
|
17
|
+
const configPath = path.resolve(CONFIG_PATH)
|
|
18
|
+
if (!(await fs.exists(configPath))) {
|
|
19
|
+
console.warn("Configuration file not found")
|
|
20
|
+
process.exit(10)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const configModule = await import(configPath)
|
|
24
|
+
const config = configModule.default as Config
|
|
25
|
+
|
|
26
|
+
switch (args[0]) {
|
|
27
|
+
case "build": {
|
|
28
|
+
await build(config)
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
case "dev": {
|
|
32
|
+
await serve(config)
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
case undefined: {
|
|
36
|
+
console.log(HELP)
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
default: {
|
|
40
|
+
console.log(
|
|
41
|
+
`The command must be either 'build' or 'dev', got ${args[0]}`,
|
|
42
|
+
)
|
|
43
|
+
process.exit(11)
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,82 +1,58 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises"
|
|
1
2
|
import { watch } from "node:fs/promises"
|
|
2
3
|
import * as path from "node:path"
|
|
3
4
|
import type { ReadableStreamDefaultController } from "node:stream/web"
|
|
4
5
|
import type { MatchedRoute } from "bun"
|
|
6
|
+
import type { Config } from "./config"
|
|
5
7
|
import { render } from "./render"
|
|
6
8
|
|
|
7
9
|
const EVENT_PATH = "/__ssg_dev_sse"
|
|
8
10
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
router: Bun.FileSystemRouter
|
|
16
|
-
|
|
17
|
-
constructor(options: {
|
|
18
|
-
pagesDir: string
|
|
19
|
-
sourceDir: string
|
|
20
|
-
assetDir?: string
|
|
21
|
-
port?: number
|
|
22
|
-
}) {
|
|
23
|
-
this.pagesDir = path.join(process.cwd(), options.pagesDir)
|
|
24
|
-
this.sourceDir = path.join(process.cwd(), options.sourceDir)
|
|
25
|
-
|
|
26
|
-
this.router = new Bun.FileSystemRouter({
|
|
27
|
-
style: "nextjs",
|
|
28
|
-
dir: options.pagesDir,
|
|
29
|
-
})
|
|
11
|
+
export async function serve(config: Config): Promise<void> {
|
|
12
|
+
const clients = new Set<ReadableStreamDefaultController>()
|
|
13
|
+
const router = new Bun.FileSystemRouter({
|
|
14
|
+
style: "nextjs",
|
|
15
|
+
dir: config.pagesDir,
|
|
16
|
+
})
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
Bun.serve({
|
|
19
|
+
port: config.port,
|
|
20
|
+
// for SSE
|
|
21
|
+
idleTimeout: 0,
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
fetch: async request => {
|
|
24
|
+
const url = new URL(request.url)
|
|
25
|
+
if (url.pathname === EVENT_PATH) {
|
|
26
|
+
return createStream(request, clients)
|
|
27
|
+
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
port: this.port,
|
|
41
|
-
// for SSE
|
|
42
|
-
idleTimeout: 0,
|
|
43
|
-
|
|
44
|
-
fetch: async request => {
|
|
45
|
-
const url = new URL(request.url)
|
|
46
|
-
if (url.pathname === EVENT_PATH) {
|
|
47
|
-
return createStream(request, clients)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const route = this.router.match(request.url)
|
|
51
|
-
if (route) {
|
|
52
|
-
return createHtml(this.pagesDir, route)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (this.assetDir) {
|
|
56
|
-
// TODO: .. escapes
|
|
57
|
-
const assetPath = path.join(
|
|
58
|
-
this.assetDir,
|
|
59
|
-
url.pathname.slice(1),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return new Response(Bun.file(assetPath))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return new Response("Not found", {
|
|
66
|
-
headers: {
|
|
67
|
-
"Content-Type": "text/html",
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
},
|
|
71
|
-
})
|
|
29
|
+
const route = router.match(url.href)
|
|
30
|
+
if (route) {
|
|
31
|
+
return createHtml(config.pagesDir, route)
|
|
32
|
+
}
|
|
72
33
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
34
|
+
const asset = await fetchStaticFile(
|
|
35
|
+
url,
|
|
36
|
+
config.assetDir,
|
|
37
|
+
)
|
|
38
|
+
if (asset) {
|
|
39
|
+
return asset
|
|
79
40
|
}
|
|
41
|
+
|
|
42
|
+
console.warn(`Path '${url.pathname}' not found`)
|
|
43
|
+
return new Response("Page or file not found", {
|
|
44
|
+
status: 404,
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
console.log(`Listening on :${config.port}`)
|
|
49
|
+
|
|
50
|
+
const watcher = watch(config.sourceDir, { recursive: true })
|
|
51
|
+
for await (const _ of watcher) {
|
|
52
|
+
router.reload()
|
|
53
|
+
clearCache(config.sourceDir)
|
|
54
|
+
for (const client of clients) {
|
|
55
|
+
client.enqueue("data: RELOAD\n\n")
|
|
80
56
|
}
|
|
81
57
|
}
|
|
82
58
|
}
|
|
@@ -108,6 +84,31 @@ function createStream(
|
|
|
108
84
|
})
|
|
109
85
|
}
|
|
110
86
|
|
|
87
|
+
async function fetchStaticFile(
|
|
88
|
+
url: URL,
|
|
89
|
+
assetDir?: string,
|
|
90
|
+
): Promise<Response | null> {
|
|
91
|
+
if (!assetDir) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let assetPath = path.join(assetDir, url.pathname.slice(1))
|
|
96
|
+
assetPath = path.resolve(assetPath)
|
|
97
|
+
if (!assetPath.startsWith(assetDir)) {
|
|
98
|
+
return new Response(
|
|
99
|
+
"Tried to get a file outside of the asset directory",
|
|
100
|
+
{ status: 403 },
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const exists = await fs.exists(assetPath)
|
|
105
|
+
if (!exists) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new Response(Bun.file(assetPath))
|
|
110
|
+
}
|
|
111
|
+
|
|
111
112
|
const RELOAD_SCRIPT = `
|
|
112
113
|
<script type="module">
|
|
113
114
|
const sse = new EventSource("${EVENT_PATH}");
|
|
@@ -141,7 +142,7 @@ async function createHtml(
|
|
|
141
142
|
.transform(response)
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
function clearCache(prefix: string) {
|
|
145
146
|
for (const path in import.meta.require.cache) {
|
|
146
147
|
if (path.startsWith(prefix)) {
|
|
147
148
|
delete import.meta.require.cache[path]
|