@kaathewise/ssg 0.5.2 → 0.6.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/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +31 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/render.d.ts +12 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +63 -0
- package/dist/run.d.ts +3 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +45 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +113 -0
- package/package.json +6 -5
- package/src/run.ts +1 -1
- package/src/build.ts +0 -37
- package/src/config.ts +0 -33
- package/src/index.ts +0 -3
- package/src/render.ts +0 -86
- package/src/server.ts +0 -151
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAKzC,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,iBA4BzC"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { Glob, write } from "bun";
|
|
4
|
+
import { renderAll } from "./render.js";
|
|
5
|
+
const glob = new Glob("**/*.{js,jsx,ts,tsx}");
|
|
6
|
+
export async function build(config) {
|
|
7
|
+
const pages = [];
|
|
8
|
+
for await (const relPath of glob.scan(config.pagesDir)) {
|
|
9
|
+
const modulePath = path.join(config.pagesDir, relPath);
|
|
10
|
+
const p = await renderAll(modulePath, config.pagesDir);
|
|
11
|
+
pages.push(...p);
|
|
12
|
+
}
|
|
13
|
+
await fs.rm(config.outputDir, { recursive: true, force: true });
|
|
14
|
+
await fs.mkdir(config.outputDir);
|
|
15
|
+
for (const page of pages) {
|
|
16
|
+
const destPath = path.join(config.outputDir, page.path);
|
|
17
|
+
const dir = path.dirname(destPath);
|
|
18
|
+
if (!(await fs.exists(dir))) {
|
|
19
|
+
await fs.mkdir(path.dirname(destPath), {
|
|
20
|
+
recursive: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const file = Bun.file(destPath);
|
|
24
|
+
await write(file, page.src);
|
|
25
|
+
}
|
|
26
|
+
if (config.assetDir) {
|
|
27
|
+
await fs.cp(config.assetDir, config.outputDir, {
|
|
28
|
+
recursive: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Config = {
|
|
2
|
+
pagesDir: string;
|
|
3
|
+
sourceDir: string;
|
|
4
|
+
assetDir?: string;
|
|
5
|
+
outputDir: string;
|
|
6
|
+
port: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function defineConfig(options: {
|
|
9
|
+
pagesDir: string;
|
|
10
|
+
sourceDir: string;
|
|
11
|
+
assetDir?: string;
|
|
12
|
+
outputDir?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
}): Config;
|
|
15
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACb,UAaA"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
const OUTPUT_DIR = "dist/";
|
|
3
|
+
const DEFAULT_PORT = 3001;
|
|
4
|
+
export function defineConfig(options) {
|
|
5
|
+
const config = {
|
|
6
|
+
pagesDir: path.resolve(options.pagesDir),
|
|
7
|
+
sourceDir: path.resolve(options.sourceDir),
|
|
8
|
+
outputDir: path.resolve(options.outputDir ?? OUTPUT_DIR),
|
|
9
|
+
port: options.port ?? DEFAULT_PORT,
|
|
10
|
+
};
|
|
11
|
+
if (options.assetDir) {
|
|
12
|
+
config.assetDir = path.resolve(options.assetDir);
|
|
13
|
+
}
|
|
14
|
+
return config;
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,KAAK,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Params {
|
|
2
|
+
[name: string]: string;
|
|
3
|
+
}
|
|
4
|
+
export type Page = {
|
|
5
|
+
path: string;
|
|
6
|
+
src: string;
|
|
7
|
+
contentType: string | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function render(modulePath: string, pagesDir: string, params?: Params): Promise<Page>;
|
|
10
|
+
export declare function renderAll(modulePath: string, pagesDir: string): Promise<Page[]>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAEA,UAAU,MAAM;IACf,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,IAAI,GAAG;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAED,wBAAsB,MAAM,CAC3B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,wBAAsB,SAAS,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,EAAE,CAAC,CAgBjB"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
export async function render(modulePath, pagesDir, params = {}) {
|
|
11
|
+
const module = await import(__rewriteRelativeImportExtension(modulePath));
|
|
12
|
+
let contentType = null;
|
|
13
|
+
if ("getContentType" in module) {
|
|
14
|
+
contentType = module.getContentType();
|
|
15
|
+
}
|
|
16
|
+
const def = module.default;
|
|
17
|
+
let src;
|
|
18
|
+
switch (typeof def) {
|
|
19
|
+
case "string": {
|
|
20
|
+
src = def;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case "function": {
|
|
24
|
+
src = await def(params);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
default: {
|
|
28
|
+
throw "Not implemented";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let pagePath = path.relative(pagesDir, modulePath);
|
|
32
|
+
pagePath = substituteParams(pagePath, params);
|
|
33
|
+
pagePath = pagePath.replace(/\.tsx?$/, "");
|
|
34
|
+
if (pagePath.endsWith("index")) {
|
|
35
|
+
pagePath += ".html";
|
|
36
|
+
}
|
|
37
|
+
return { path: pagePath, src, contentType };
|
|
38
|
+
}
|
|
39
|
+
export async function renderAll(modulePath, pagesDir) {
|
|
40
|
+
const module = await import(__rewriteRelativeImportExtension(modulePath));
|
|
41
|
+
const out = [];
|
|
42
|
+
if ("getStaticParams" in module) {
|
|
43
|
+
const paramsList = await module.getStaticParams();
|
|
44
|
+
for (const params of paramsList) {
|
|
45
|
+
out.push(await render(modulePath, pagesDir, params));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out.push(await render(modulePath, pagesDir));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function substituteParams(inputPath, params) {
|
|
54
|
+
let path = inputPath;
|
|
55
|
+
for (const [key, value] of Object.entries(params)) {
|
|
56
|
+
const single = `[${key}]`;
|
|
57
|
+
const multiple = `[[...${key}]]`;
|
|
58
|
+
path = path.replace(single, value);
|
|
59
|
+
path = path.replace(multiple, value);
|
|
60
|
+
}
|
|
61
|
+
path = path.replace(/\.tsx?$/, "");
|
|
62
|
+
return path;
|
|
63
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":""}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
3
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
4
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
5
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return path;
|
|
9
|
+
};
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { build, serve } from "./index.js";
|
|
13
|
+
const CONFIG_PATH = "ssg.config.ts";
|
|
14
|
+
const HELP = `Usage: ssg <command>
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
build: build the website
|
|
18
|
+
dev: start a hot-reloading development server
|
|
19
|
+
`;
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const configPath = path.resolve(CONFIG_PATH);
|
|
22
|
+
if (!(await fs.exists(configPath))) {
|
|
23
|
+
console.warn("Configuration file not found");
|
|
24
|
+
process.exit(10);
|
|
25
|
+
}
|
|
26
|
+
const configModule = await import(__rewriteRelativeImportExtension(configPath));
|
|
27
|
+
const config = configModule.default;
|
|
28
|
+
switch (args[0]) {
|
|
29
|
+
case "build": {
|
|
30
|
+
await build(config);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "dev": {
|
|
34
|
+
await serve(config);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case undefined: {
|
|
38
|
+
console.log(HELP);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
default: {
|
|
42
|
+
console.log(`The command must be either 'build' or 'dev', got ${args[0]}`);
|
|
43
|
+
process.exit(11);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAKzC,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CzD"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { watch } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { render } from "./render.js";
|
|
5
|
+
const EVENT_PATH = "/__ssg_dev_sse";
|
|
6
|
+
export async function serve(config) {
|
|
7
|
+
const clients = new Set();
|
|
8
|
+
const router = new Bun.FileSystemRouter({
|
|
9
|
+
style: "nextjs",
|
|
10
|
+
dir: config.pagesDir,
|
|
11
|
+
});
|
|
12
|
+
Bun.serve({
|
|
13
|
+
port: config.port,
|
|
14
|
+
// for SSE
|
|
15
|
+
idleTimeout: 0,
|
|
16
|
+
fetch: async (request) => {
|
|
17
|
+
const url = new URL(request.url);
|
|
18
|
+
if (url.pathname === EVENT_PATH) {
|
|
19
|
+
return createStream(request, clients);
|
|
20
|
+
}
|
|
21
|
+
const route = router.match(url.href);
|
|
22
|
+
if (route) {
|
|
23
|
+
return createHtml(config.pagesDir, route);
|
|
24
|
+
}
|
|
25
|
+
const asset = await fetchStaticFile(url, config.assetDir);
|
|
26
|
+
if (asset) {
|
|
27
|
+
return asset;
|
|
28
|
+
}
|
|
29
|
+
console.warn(`Path '${url.pathname}' not found`);
|
|
30
|
+
return new Response("Page or file not found", {
|
|
31
|
+
status: 404,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
console.log(`Listening on :${config.port}`);
|
|
36
|
+
const watcher = watch(config.sourceDir, { recursive: true });
|
|
37
|
+
for await (const _ of watcher) {
|
|
38
|
+
router.reload();
|
|
39
|
+
clearCache(config.sourceDir);
|
|
40
|
+
for (const client of clients) {
|
|
41
|
+
client.enqueue("data: RELOAD\n\n");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function createStream(request, clients) {
|
|
46
|
+
const stream = new ReadableStream({
|
|
47
|
+
start(controller) {
|
|
48
|
+
clients.add(controller);
|
|
49
|
+
// workaround because @ts-expect-error doesn't fail for
|
|
50
|
+
// aspartik/website for some reason, breaking the check.
|
|
51
|
+
// biome-ignore lint/suspicious/noExplicitAny: above
|
|
52
|
+
const signal = request.signal;
|
|
53
|
+
signal.addEventListener("abort", () => {
|
|
54
|
+
controller.close();
|
|
55
|
+
clients.delete(controller);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return new Response(stream, {
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "text/event-stream",
|
|
62
|
+
"Cache-Control": "no-cache",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function fetchStaticFile(url, assetDir) {
|
|
67
|
+
if (!assetDir) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
let assetPath = path.join(assetDir, url.pathname.slice(1));
|
|
71
|
+
assetPath = path.resolve(assetPath);
|
|
72
|
+
if (!assetPath.startsWith(assetDir)) {
|
|
73
|
+
return new Response("Tried to get a file outside of the asset directory", { status: 403 });
|
|
74
|
+
}
|
|
75
|
+
const exists = await fs.exists(assetPath);
|
|
76
|
+
if (!exists) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return new Response(Bun.file(assetPath));
|
|
80
|
+
}
|
|
81
|
+
const RELOAD_SCRIPT = `
|
|
82
|
+
<script type="module">
|
|
83
|
+
const sse = new EventSource("${EVENT_PATH}");
|
|
84
|
+
sse.onmessage = function(msg) {
|
|
85
|
+
if (msg.data === "RELOAD") {
|
|
86
|
+
location.reload()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
window.addEventListener("beforeunload", () => sse.close())
|
|
90
|
+
</script>
|
|
91
|
+
`;
|
|
92
|
+
async function createHtml(pagesDir, route) {
|
|
93
|
+
const page = await render(route.filePath, pagesDir, route.params);
|
|
94
|
+
const response = new Response(page.src, {
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": page.contentType ?? "text/html",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return new HTMLRewriter()
|
|
100
|
+
.onDocument({
|
|
101
|
+
end: (el) => {
|
|
102
|
+
el.append(RELOAD_SCRIPT, { html: true });
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.transform(response);
|
|
106
|
+
}
|
|
107
|
+
function clearCache(prefix) {
|
|
108
|
+
for (const path in import.meta.require.cache) {
|
|
109
|
+
if (path.startsWith(prefix)) {
|
|
110
|
+
delete import.meta.require.cache[path];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaathewise/ssg",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "JSX static site generator",
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "Andrej Kolčin",
|
|
7
7
|
"type": "module",
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
"url": "git+https://github.com/kaathewisegit/ssg.git"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"check": "biome check && tsc --noEmit"
|
|
13
|
+
"check": "biome check && tsc --noEmit",
|
|
14
|
+
"build": "tsc --project tsconfig.build.json"
|
|
14
15
|
},
|
|
15
|
-
"main": "
|
|
16
|
+
"main": "dist/index.ts",
|
|
16
17
|
"bin": {
|
|
17
18
|
"ssg": "src/run.ts"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
|
-
"
|
|
21
|
+
"dist"
|
|
21
22
|
],
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@biomejs/biome": "^2.2.3",
|
package/src/run.ts
CHANGED
package/src/build.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises"
|
|
2
|
-
import * as path from "node:path"
|
|
3
|
-
import { Glob, write } from "bun"
|
|
4
|
-
import type { Config } from "./config"
|
|
5
|
-
import { type Page, renderAll } from "./render"
|
|
6
|
-
|
|
7
|
-
const glob = new Glob("**/*.{js,jsx,ts,tsx}")
|
|
8
|
-
|
|
9
|
-
export async function build(config: Config) {
|
|
10
|
-
const pages: Page[] = []
|
|
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)
|
|
14
|
-
pages.push(...p)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
await fs.rm(config.outputDir, { recursive: true, force: true })
|
|
18
|
-
await fs.mkdir(config.outputDir)
|
|
19
|
-
|
|
20
|
-
for (const page of pages) {
|
|
21
|
-
const destPath = path.join(config.outputDir, page.path)
|
|
22
|
-
const dir = path.dirname(destPath)
|
|
23
|
-
if (!(await fs.exists(dir))) {
|
|
24
|
-
await fs.mkdir(path.dirname(destPath), {
|
|
25
|
-
recursive: true,
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
const file = Bun.file(destPath)
|
|
29
|
-
await write(file, page.src)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (config.assetDir) {
|
|
33
|
-
await fs.cp(config.assetDir, config.outputDir, {
|
|
34
|
-
recursive: true,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
DELETED
package/src/render.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as path from "node:path"
|
|
2
|
-
|
|
3
|
-
interface Params {
|
|
4
|
-
[name: string]: string
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type Page = {
|
|
8
|
-
path: string
|
|
9
|
-
src: string
|
|
10
|
-
contentType: string | null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function render(
|
|
14
|
-
modulePath: string,
|
|
15
|
-
pagesDir: string,
|
|
16
|
-
params: Params = {},
|
|
17
|
-
): Promise<Page> {
|
|
18
|
-
const module = await import(modulePath)
|
|
19
|
-
|
|
20
|
-
let contentType = null
|
|
21
|
-
if ("getContentType" in module) {
|
|
22
|
-
contentType = module.getContentType()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const def = module.default
|
|
26
|
-
|
|
27
|
-
let src: string
|
|
28
|
-
switch (typeof def) {
|
|
29
|
-
case "string": {
|
|
30
|
-
src = def
|
|
31
|
-
break
|
|
32
|
-
}
|
|
33
|
-
case "function": {
|
|
34
|
-
src = await def(params)
|
|
35
|
-
break
|
|
36
|
-
}
|
|
37
|
-
default: {
|
|
38
|
-
throw "Not implemented"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let pagePath = path.relative(pagesDir, modulePath)
|
|
43
|
-
pagePath = substituteParams(pagePath, params)
|
|
44
|
-
pagePath = pagePath.replace(/\.tsx?$/, "")
|
|
45
|
-
if (pagePath.endsWith("index")) {
|
|
46
|
-
pagePath += ".html"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { path: pagePath, src, contentType }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function renderAll(
|
|
53
|
-
modulePath: string,
|
|
54
|
-
pagesDir: string,
|
|
55
|
-
): Promise<Page[]> {
|
|
56
|
-
const module = await import(modulePath)
|
|
57
|
-
|
|
58
|
-
const out: Page[] = []
|
|
59
|
-
|
|
60
|
-
if ("getStaticParams" in module) {
|
|
61
|
-
const paramsList: Params[] = await module.getStaticParams()
|
|
62
|
-
|
|
63
|
-
for (const params of paramsList) {
|
|
64
|
-
out.push(await render(modulePath, pagesDir, params))
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
out.push(await render(modulePath, pagesDir))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return out
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function substituteParams(inputPath: string, params: Params): string {
|
|
74
|
-
let path = inputPath
|
|
75
|
-
for (const [key, value] of Object.entries(params)) {
|
|
76
|
-
const single = `[${key}]`
|
|
77
|
-
const multiple = `[[...${key}]]`
|
|
78
|
-
|
|
79
|
-
path = path.replace(single, value)
|
|
80
|
-
path = path.replace(multiple, value)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
path = path.replace(/\.tsx?$/, "")
|
|
84
|
-
|
|
85
|
-
return path
|
|
86
|
-
}
|
package/src/server.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises"
|
|
2
|
-
import { watch } from "node:fs/promises"
|
|
3
|
-
import * as path from "node:path"
|
|
4
|
-
import type { ReadableStreamDefaultController } from "node:stream/web"
|
|
5
|
-
import type { MatchedRoute } from "bun"
|
|
6
|
-
import type { Config } from "./config"
|
|
7
|
-
import { render } from "./render"
|
|
8
|
-
|
|
9
|
-
const EVENT_PATH = "/__ssg_dev_sse"
|
|
10
|
-
|
|
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
|
-
})
|
|
17
|
-
|
|
18
|
-
Bun.serve({
|
|
19
|
-
port: config.port,
|
|
20
|
-
// for SSE
|
|
21
|
-
idleTimeout: 0,
|
|
22
|
-
|
|
23
|
-
fetch: async request => {
|
|
24
|
-
const url = new URL(request.url)
|
|
25
|
-
if (url.pathname === EVENT_PATH) {
|
|
26
|
-
return createStream(request, clients)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const route = router.match(url.href)
|
|
30
|
-
if (route) {
|
|
31
|
-
return createHtml(config.pagesDir, route)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const asset = await fetchStaticFile(
|
|
35
|
-
url,
|
|
36
|
-
config.assetDir,
|
|
37
|
-
)
|
|
38
|
-
if (asset) {
|
|
39
|
-
return asset
|
|
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")
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function createStream(
|
|
61
|
-
request: Request,
|
|
62
|
-
clients: Set<ReadableStreamDefaultController>,
|
|
63
|
-
) {
|
|
64
|
-
const stream = new ReadableStream({
|
|
65
|
-
start(controller): void {
|
|
66
|
-
clients.add(controller)
|
|
67
|
-
|
|
68
|
-
// workaround because @ts-expect-error doesn't fail for
|
|
69
|
-
// aspartik/website for some reason, breaking the check.
|
|
70
|
-
// biome-ignore lint/suspicious/noExplicitAny: above
|
|
71
|
-
const signal = request.signal as any
|
|
72
|
-
signal.addEventListener("abort", () => {
|
|
73
|
-
controller.close()
|
|
74
|
-
clients.delete(controller)
|
|
75
|
-
})
|
|
76
|
-
},
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
return new Response(stream, {
|
|
80
|
-
headers: {
|
|
81
|
-
"Content-Type": "text/event-stream",
|
|
82
|
-
"Cache-Control": "no-cache",
|
|
83
|
-
},
|
|
84
|
-
})
|
|
85
|
-
}
|
|
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
|
-
|
|
112
|
-
const RELOAD_SCRIPT = `
|
|
113
|
-
<script type="module">
|
|
114
|
-
const sse = new EventSource("${EVENT_PATH}");
|
|
115
|
-
sse.onmessage = function(msg) {
|
|
116
|
-
if (msg.data === "RELOAD") {
|
|
117
|
-
location.reload()
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
window.addEventListener("beforeunload", () => sse.close())
|
|
121
|
-
</script>
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
async function createHtml(
|
|
125
|
-
pagesDir: string,
|
|
126
|
-
route: MatchedRoute,
|
|
127
|
-
): Promise<Response> {
|
|
128
|
-
const page = await render(route.filePath, pagesDir, route.params)
|
|
129
|
-
|
|
130
|
-
const response = new Response(page.src, {
|
|
131
|
-
headers: {
|
|
132
|
-
"Content-Type": page.contentType ?? "text/html",
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
return new HTMLRewriter()
|
|
137
|
-
.onDocument({
|
|
138
|
-
end: (el): void => {
|
|
139
|
-
el.append(RELOAD_SCRIPT, { html: true })
|
|
140
|
-
},
|
|
141
|
-
})
|
|
142
|
-
.transform(response)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function clearCache(prefix: string) {
|
|
146
|
-
for (const path in import.meta.require.cache) {
|
|
147
|
-
if (path.startsWith(prefix)) {
|
|
148
|
-
delete import.meta.require.cache[path]
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|