@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaathewise/ssg",
3
3
  "description": "Bun & JSX static site generator",
4
- "version": "0.3.1",
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(pagesDir: string, outDir: string = "dist/") {
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(outDir, { recursive: true, force: true })
20
- await fs.mkdir(outDir)
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(outDir, page.path)
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
@@ -1,3 +1,4 @@
1
1
  export { build } from "./build"
2
+ export { type Config, defineConfig } from "./config"
2
3
  export { postcssPlugin } from "./css"
3
- export { Server } from "./server"
4
+ export { serve } from "./server"
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 class Server {
10
- pagesDir: string
11
- sourceDir: string
12
- assetDir?: string
13
- port?: number = 3001
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
- this.assetDir ??= options.assetDir
18
+ Bun.serve({
19
+ port: config.port,
20
+ // for SSE
21
+ idleTimeout: 0,
32
22
 
33
- this.port ??= options?.port
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
- async listen() {
37
- const clients = new Set<ReadableStreamDefaultController>()
38
-
39
- Bun.serve({
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
- const watcher = watch(this.sourceDir, { recursive: true })
74
- for await (const _ of watcher) {
75
- this.router.reload()
76
- clearCache(this.sourceDir)
77
- for (const client of clients) {
78
- client.enqueue("data: RELOAD\n\n")
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
- export function clearCache(prefix: string) {
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]