authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-06-08 17:31:03-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:44-07:00
logad50869c177bec4aa2eb956e55acbee3ba04ad27
tree205b5fb1ccac84685e481d51a4bc3d7ceebb303b
parent8421676693001c607e4963009031d205fa94bbe6
signature Commit is signed but in an unrecognized format.

more organize


24 files changed, 372 insertions(+), 331 deletions(-)

framework/backend/entry-passthru.ts created+4
......@@ -0,0 +1,4 @@
1import "@paperclover/console/inject";
2export default app;
3
4import app from "#backend";
framework/css.ts+2-3
......@@ -64,7 +64,7 @@ export async function bundleCssFiles(
6464 }),
6565 );
6666 },
67 } satisfies Plugin;
67 } satisfies esbuild.Plugin;
6868 const build = await esbuild.build({
6969 bundle: true,
7070 entryPoints: ["$input$"],
......@@ -85,6 +85,5 @@ export async function bundleCssFiles(
8585 return outputFiles[0].text;
8686}
8787
88import type { Plugin } from "esbuild";
8988import * as esbuild from "esbuild";
90import * as fs from "./fs.ts";
89import * as fs from "#sitegen/fs";
framework/fs.ts deleted-61
......@@ -1,61 +0,0 @@
1// File System APIs. Some custom APIs, but mostly a re-export a mix of built-in
2// Node.js sync+promise fs methods. For convenince.
3export {
4 existsSync,
5 readdir,
6 readdirSync,
7 readFile,
8 readFileSync,
9 rm,
10 rmSync,
11 stat,
12 statSync,
13 writeFile,
14 writeFileSync,
15};
16
17export function mkdir(dir: string) {
18 return nodeMkdir(dir, { recursive: true });
19}
20
21export function mkdirSync(dir: string) {
22 return nodeMkdirSync(dir, { recursive: true });
23}
24
25export async function writeMkdir(file: string, contents: Buffer | string) {
26 await mkdir(path.dirname(file));
27 return writeFile(file, contents);
28}
29
30export function writeMkdirSync(file: string, contents: Buffer | string) {
31 mkdirSync(path.dirname(file));
32 return writeFileSync(file, contents);
33}
34
35export function readDirRecOptionalSync(dir: string) {
36 try {
37 return readdirSync(dir, { recursive: true, withFileTypes: true });
38 } catch (err: any) {
39 if (err.code === "ENOENT") return [];
40 throw err;
41 }
42}
43
44import * as path from "node:path";
45import {
46 existsSync,
47 mkdirSync as nodeMkdirSync,
48 readdirSync,
49 readFileSync,
50 rmSync,
51 statSync,
52 writeFileSync,
53} from "node:fs";
54import {
55 mkdir as nodeMkdir,
56 readdir,
57 readFile,
58 rm,
59 stat,
60 writeFile,
61} from "node:fs/promises";
framework/generate.tsx+4-4
......@@ -290,10 +290,10 @@ import { OnceMap, Queue } from "./queue.ts";
290290import { Incremental } from "./incremental.ts";
291291import * as bundle from "./bundle.ts";
292292import * as css from "./css.ts";
293import * as fs from "./fs.ts";
294import { Spinner, withSpinner } from "@paperclover/console/Spinner";
295import * as meta from "./meta.ts";
296293import * as ssr from "./engine/ssr.ts";
297import * as sg from "#sitegen";
298294import * as hot from "./hot.ts";
295import * as fs from "#sitegen/fs";
296import * as sg from "#sitegen";
299297import * as path from "node:path";
298import * as meta from "#sitegen/meta";
299import { Spinner, withSpinner } from "@paperclover/console/Spinner";
framework/hot.ts+1-1
......@@ -236,7 +236,7 @@ declare module "node:module" {
236236 ): unknown;
237237}
238238
239import * as fs from "./fs.ts";
239import * as fs from "./lib/fs.ts";
240240import * as path from "node:path";
241241import { pathToFileURL } from "node:url";
242242import * as esbuild from "esbuild";
framework/incremental.ts+2-3
......@@ -252,11 +252,10 @@ export interface SerializedMeta {
252252 script: [key: string, value: string][];
253253}
254254
255import * as path from "node:path";
256import * as fs from "./fs.ts";
255import * as fs from "#sitegen/fs";
257256import * as zlib from "node:zlib";
258257import * as util from "node:util";
259258import { Queue } from "./queue.ts";
260259import * as hot from "./hot.ts";
261import * as mime from "./mime.ts";
260import * as mime from "#sitegen/mime";
262261import { Buffer } from "node:buffer";
framework/lib/assets.ts+1-1
......@@ -105,7 +105,7 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
105105 return c.res = new Response(body, { headers, status });
106106}
107107
108import * as fs from "../fs.ts";
108import * as fs from "#sitegen/fs";
109109import type { Context, Next } from "hono";
110110import type { StatusCode } from "hono/utils/http-status";
111111import type { BuiltAsset, BuiltAssetMap, View } from "../incremental.ts";
framework/lib/fs.ts created+61
......@@ -0,0 +1,61 @@
1// File System APIs. Some custom APIs, but mostly a re-export a mix of built-in
2// Node.js sync+promise fs methods. For convenince.
3export {
4 existsSync,
5 readdir,
6 readdirSync,
7 readFile,
8 readFileSync,
9 rm,
10 rmSync,
11 stat,
12 statSync,
13 writeFile,
14 writeFileSync,
15};
16
17export function mkdir(dir: string) {
18 return nodeMkdir(dir, { recursive: true });
19}
20
21export function mkdirSync(dir: string) {
22 return nodeMkdirSync(dir, { recursive: true });
23}
24
25export async function writeMkdir(file: string, contents: Buffer | string) {
26 await mkdir(path.dirname(file));
27 return writeFile(file, contents);
28}
29
30export function writeMkdirSync(file: string, contents: Buffer | string) {
31 mkdirSync(path.dirname(file));
32 return writeFileSync(file, contents);
33}
34
35export function readDirRecOptionalSync(dir: string) {
36 try {
37 return readdirSync(dir, { recursive: true, withFileTypes: true });
38 } catch (err: any) {
39 if (err.code === "ENOENT") return [];
40 throw err;
41 }
42}
43
44import * as path from "node:path";
45import {
46 existsSync,
47 mkdirSync as nodeMkdirSync,
48 readdirSync,
49 readFileSync,
50 rmSync,
51 statSync,
52 writeFileSync,
53} from "node:fs";
54import {
55 mkdir as nodeMkdir,
56 readdir,
57 readFile,
58 rm,
59 stat,
60 writeFile,
61} from "node:fs/promises";
framework/lib/meta.ts created+24
......@@ -0,0 +1,24 @@
1export interface Meta {
2 title: string;
3 description?: string | undefined;
4 openGraph?: OpenGraph;
5 alternates?: Alternates;
6}
7export interface OpenGraph {
8 title?: string;
9 description?: string | undefined;
10 type: string;
11 url: string;
12}
13export interface Alternates {
14 canonical: string;
15 types: { [mime: string]: AlternateType };
16}
17export interface AlternateType {
18 url: string;
19 title: string;
20}
21export function renderMeta({ title }: Meta): string {
22 return `<title>${esc(title)}</title>`;
23}
24import { escapeHtml as esc } from "../engine/ssr.ts";
framework/lib/mime.ts created+31
......@@ -0,0 +1,31 @@
1const entries = fs.readFileSync(
2 path.join(import.meta.dirname, "mime.txt"),
3 "utf8",
4)
5 .split("\n")
6 .map((line) => line.trim())
7 .filter((line) => line && !line.startsWith("#"))
8 .map((line) => line.split(/\s+/, 2) as [string, string]);
9const extensions = new Map(entries.filter((x) => x[0].startsWith(".")));
10const fullNames = new Map(entries.filter((x) => !x[0].startsWith(".")));
11
12/**
13 * Accepts:
14 * - Full file path or basename
15 * - Extension (with or without dot)
16 */
17export function contentTypeFor(file: string) {
18 const slash = file.indexOf("/");
19 if (slash !== -1) file = file.slice(slash + 1);
20 const dot = file.indexOf(".");
21 if (dot === -1) file = "." + file;
22 else if (dot > 0) {
23 let entry = fullNames.get(file);
24 if (entry) return entry;
25 file = file.slice(dot);
26 }
27 return extensions.get(file) ?? "application/octet-stream";
28}
29
30import * as fs from "#sitegen/fs";
31import * as path from "node:path";
framework/lib/mime.txt created+99
......@@ -0,0 +1,99 @@
1# media types
2# https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types
3.aac audio/x-aac
4.aif audio/x-aiff
5.aifc audio/x-aiff
6.aiff audio/x-aiff
7.asm text/x-asm
8.avi video/x-msvideo
9.bat application/x-msdownload
10.c text/x-c
11.chat text/x-clover-chatlog
12.class application/java-vm
13.cmd application/x-msdownload
14.com application/x-msdownload
15.conf text/plain
16.cpp text/x-c
17.css text/css
18.csv text/csv
19.cxx text/x-c
20.def text/plain
21.diff text/plain
22.dll application/x-msdownload
23.dmg application/octet-stream
24.doc application/msword
25.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
26.epub application/epub+zip
27.exe application/x-msdownload
28.flv video/x-flv
29.fbx application/fbx
30.gz application/x-gzip
31.h text/x-c
32.h264 video/h264
33.hh text/x-c
34.htm text/html;charset=utf-8
35.html text/html;charset=utf-8
36.ico image/x-icon
37.ics text/calendar
38.in text/plain
39.jar application/java-archive
40.java text/x-java-source
41.jpeg image/jpeg
42.jpg image/jpeg
43.jpgv video/jpeg
44.jxl image/jxl
45.js application/javascript
46.json application/json
47.latex application/x-latex
48.list text/plain
49.log text/plain
50.m4a audio/mp4
51.man text/troff
52.mid audio/midi
53.midi audio/midi
54.mov video/quicktime
55.mp3 audio/mpeg
56.mp4 video/mp4
57.msh model/mesh
58.msi application/x-msdownload
59.obj application/octet-stream
60.ogg audio/ogg
61.otf application/x-font-otf
62.pdf application/pdf
63.png image/png
64.ppt application/vnd.ms-powerpoint
65.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
66.psd image/vnd.adobe.photoshop
67.py text/x-python
68.rar application/x-rar-compressed
69.rss application/rss+xml
70.rtf application/rtf
71.rtx text/richtext
72.s text/x-asm
73.pem application/x-pem-file"
74.ser application/java-serialized-object
75.sh application/x-sh
76.sig application/pgp-signature
77.silo model/mesh
78.svg image/svg+xml
79.t text/troff
80.tar application/x-tar
81.text text/plain
82.tgz application/x-gzip
83.tif image/tiff
84.tiff image/tiff
85.torrent application/x-bittorrent
86.ttc application/x-font-ttf
87.ttf application/x-font-ttf
88.txt text/plain
89.urls text/uri-list
90.v text/x-v
91.wav audio/x-wav
92.wmv video/x-ms-wmv
93.xls application/vnd.ms-excel
94.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
95.xml application/xml
96.xps application/vnd.ms-xpsdocument
97
98# special cased based on file name
99rss.xml application/rss+xml
framework/lib/sqlite.ts created+97
......@@ -0,0 +1,97 @@
1// Guard against reloads and bundler duplication.
2// @ts-ignore
3const map = globalThis[Symbol.for("clover.db")] ??= new Map<
4 string,
5 WrappedDatabase
6>();
7
8export function getDb(file: string) {
9 let db: WrappedDatabase | null = map.get(file);
10 if (db) return db;
11 const fileWithExt = file.includes(".") ? file : file + ".sqlite";
12 db = new WrappedDatabase(
13 new DatabaseSync(path.join(".clover/", fileWithExt)),
14 );
15 map.set(file, db);
16 return db;
17}
18
19export class WrappedDatabase {
20 node: DatabaseSync;
21 stmtTableMigrate: WeakRef<StatementSync> | null = null;
22
23 constructor(node: DatabaseSync) {
24 this.node = node;
25 this.node.exec(`
26 create table if not exists clover_migrations (
27 key text not null primary key,
28 version integer not null
29 );
30 `);
31 }
32
33 // TODO: add migration support
34 // the idea is you keep `schema` as the new schema but can add
35 // migrations to the mix really easily.
36 table(name: string, schema: string) {
37 let s = this.stmtTableMigrate?.deref();
38 s ?? (this.stmtTableMigrate = new WeakRef(
39 s = this.node.prepare(`
40 insert or ignore into clover_migrations
41 (key, version) values (?, ?);
42 `),
43 ));
44 const { changes } = s.run(name, 1);
45 if (changes === 1) this.node.exec(schema);
46 }
47
48 prepare<Args extends unknown[] = [], Result = unknown>(
49 query: string,
50 ): Stmt<Args, Result> {
51 return new Stmt(this.node.prepare(query));
52 }
53}
54
55export class Stmt<Args extends unknown[] = unknown[], Row = unknown> {
56 #node: StatementSync;
57 #class: any | null = null;
58 constructor(node: StatementSync) {
59 this.#node = node;
60 }
61
62 /** Get one row */
63 get(...args: Args): Row | null {
64 const item = this.#node.get(...args as any) as Row;
65 if (!item) return null;
66 const C = this.#class;
67 if (C) Object.setPrototypeOf(item, C.prototype);
68 return item;
69 }
70 getNonNull(...args: Args) {
71 const item = this.get(...args);
72 if (!item) throw new Error("Query returned no result");
73 return item;
74 }
75 iter(...args: Args): Iterator<Row> {
76 return this.array(...args)[Symbol.iterator]();
77 }
78 /** Get all rows */
79 array(...args: Args): Row[] {
80 const array = this.#node.all(...args as any) as Row[];
81 const C = this.#class;
82 if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype));
83 return array;
84 }
85 /** Return the number of changes / row ID */
86 run(...args: Args) {
87 return this.#node.run(...args as any);
88 }
89
90 as<R>(Class: { new (): R }): Stmt<Args, R> {
91 this.#class = Class;
92 return this as any;
93 }
94}
95
96import { DatabaseSync, StatementSync } from "node:sqlite";
97import * as path from "node:path";
framework/meta.ts deleted-24
......@@ -1,24 +0,0 @@
1export interface Meta {
2 title: string;
3 description?: string | undefined;
4 openGraph?: OpenGraph;
5 alternates?: Alternates;
6}
7export interface OpenGraph {
8 title?: string;
9 description?: string | undefined;
10 type: string;
11 url: string;
12}
13export interface Alternates {
14 canonical: string;
15 types: { [mime: string]: AlternateType };
16}
17export interface AlternateType {
18 url: string;
19 title: string;
20}
21export function renderMeta({ title }: Meta): string {
22 return `<title>${esc(title)}</title>`;
23}
24import { escapeHtml as esc } from "./engine/ssr.ts";
framework/mime.ts deleted-31
......@@ -1,31 +0,0 @@
1const entries = fs.readFileSync(
2 path.join(import.meta.dirname, "mime.txt"),
3 "utf8",
4)
5 .split("\n")
6 .map((line) => line.trim())
7 .filter((line) => line && !line.startsWith("#"))
8 .map((line) => line.split(/\s+/, 2) as [string, string]);
9const extensions = new Map(entries.filter((x) => x[0].startsWith(".")));
10const fullNames = new Map(entries.filter((x) => !x[0].startsWith(".")));
11
12/**
13 * Accepts:
14 * - Full file path or basename
15 * - Extension (with or without dot)
16 */
17export function contentTypeFor(file: string) {
18 const slash = file.indexOf("/");
19 if (slash !== -1) file = file.slice(slash + 1);
20 const dot = file.indexOf(".");
21 if (dot === -1) file = "." + file;
22 else if (dot > 0) {
23 let entry = fullNames.get(file);
24 if (entry) return entry;
25 file = file.slice(dot);
26 }
27 return extensions.get(file) ?? "application/octet-stream";
28}
29
30import * as fs from "./fs.ts";
31import * as path from "node:path";
framework/mime.txt deleted-99
......@@ -1,99 +0,0 @@
1# media types
2# https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types
3.aac audio/x-aac
4.aif audio/x-aiff
5.aifc audio/x-aiff
6.aiff audio/x-aiff
7.asm text/x-asm
8.avi video/x-msvideo
9.bat application/x-msdownload
10.c text/x-c
11.chat text/x-clover-chatlog
12.class application/java-vm
13.cmd application/x-msdownload
14.com application/x-msdownload
15.conf text/plain
16.cpp text/x-c
17.css text/css
18.csv text/csv
19.cxx text/x-c
20.def text/plain
21.diff text/plain
22.dll application/x-msdownload
23.dmg application/octet-stream
24.doc application/msword
25.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
26.epub application/epub+zip
27.exe application/x-msdownload
28.flv video/x-flv
29.fbx application/fbx
30.gz application/x-gzip
31.h text/x-c
32.h264 video/h264
33.hh text/x-c
34.htm text/html;charset=utf-8
35.html text/html;charset=utf-8
36.ico image/x-icon
37.ics text/calendar
38.in text/plain
39.jar application/java-archive
40.java text/x-java-source
41.jpeg image/jpeg
42.jpg image/jpeg
43.jpgv video/jpeg
44.jxl image/jxl
45.js application/javascript
46.json application/json
47.latex application/x-latex
48.list text/plain
49.log text/plain
50.m4a audio/mp4
51.man text/troff
52.mid audio/midi
53.midi audio/midi
54.mov video/quicktime
55.mp3 audio/mpeg
56.mp4 video/mp4
57.msh model/mesh
58.msi application/x-msdownload
59.obj application/octet-stream
60.ogg audio/ogg
61.otf application/x-font-otf
62.pdf application/pdf
63.png image/png
64.ppt application/vnd.ms-powerpoint
65.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
66.psd image/vnd.adobe.photoshop
67.py text/x-python
68.rar application/x-rar-compressed
69.rss application/rss+xml
70.rtf application/rtf
71.rtx text/richtext
72.s text/x-asm
73.pem application/x-pem-file"
74.ser application/java-serialized-object
75.sh application/x-sh
76.sig application/pgp-signature
77.silo model/mesh
78.svg image/svg+xml
79.t text/troff
80.tar application/x-tar
81.text text/plain
82.tgz application/x-gzip
83.tif image/tiff
84.tiff image/tiff
85.torrent application/x-bittorrent
86.ttc application/x-font-ttf
87.ttf application/x-font-ttf
88.txt text/plain
89.urls text/uri-list
90.v text/x-v
91.wav audio/x-wav
92.wmv video/x-ms-wmv
93.xls application/vnd.ms-excel
94.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
95.xml application/xml
96.xps application/vnd.ms-xpsdocument
97
98# special cased based on file name
99rss.xml application/rss+xml
framework/sqlite.ts deleted-98
......@@ -1,98 +0,0 @@
1// Guard against reloads and bundler duplication.
2// @ts-ignore
3const map = globalThis[Symbol.for("clover.db")] ??= new Map<
4 string,
5 WrappedDatabase
6>();
7
8export function getDb(file: string) {
9 let db: WrappedDatabase | null = map.get(file);
10 if (db) return db;
11 const fileWithExt = file.includes(".") ? file : file + ".sqlite";
12 db = new WrappedDatabase(
13 new DatabaseSync(path.join(".clover/", fileWithExt)),
14 );
15 map.set(file, db);
16 return db;
17}
18
19export class WrappedDatabase {
20 node: DatabaseSync;
21 stmtTableMigrate: WeakRef<StatementSync> | null = null;
22
23 constructor(node: DatabaseSync) {
24 this.node = node;
25 this.node.exec(`
26 create table if not exists clover_migrations (
27 key text not null primary key,
28 version integer not null
29 );
30 `);
31 }
32
33 // TODO: add migration support
34 // the idea is you keep `schema` as the new schema but can add
35 // migrations to the mix really easily.
36 table(name: string, schema: string) {
37 let s = this.stmtTableMigrate?.deref();
38 s ?? (this.stmtTableMigrate = new WeakRef(
39 s = this.node.prepare(`
40 insert or ignore into clover_migrations
41 (key, version) values (?, ?);
42 `),
43 ));
44 const { changes } = s.run(name, 1);
45 if (changes === 1) this.node.exec(schema);
46 }
47
48 prepare<Args extends unknown[] = [], Result = unknown>(
49 query: string,
50 ): Stmt<Args, Result> {
51 return new Stmt(this.node.prepare(query));
52 }
53}
54
55export class Stmt<Args extends unknown[] = unknown[], Row = unknown> {
56 #node: StatementSync;
57 #class: any | null = null;
58 constructor(node: StatementSync) {
59 this.#node = node;
60 }
61
62 /** Get one row */
63 get(...args: Args): Row | null {
64 const item = this.#node.get(...args as any) as Row;
65 if (!item) return null;
66 const C = this.#class;
67 if (C) Object.setPrototypeOf(item, C.prototype);
68 return item;
69 }
70 getNonNull(...args: Args) {
71 const item = this.get(...args);
72 if (!item) throw new Error("Query returned no result");
73 return item;
74 }
75 iter(...args: Args): Iterator<Row> {
76 return this.array(...args)[Symbol.iterator]();
77 }
78 /** Get all rows */
79 array(...args: Args): Row[] {
80 const array = this.#node.all(...args as any) as Row[];
81 const C = this.#class;
82 if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype));
83 return array;
84 }
85 /** Return the number of changes / row ID */
86 run(...args: Args) {
87 return this.#node.run(...args as any);
88 }
89
90 as<R>(Class: { new (): R }): Stmt<Args, R> {
91 this.#class = Class;
92 return this as any;
93 }
94}
95
96import { DatabaseSync, StatementSync } from "node:sqlite";
97import * as fs from "./fs.ts";
98import * as path from "node:path";
framework/typecheck.ts deleted
package-lock.json+1-1
......@@ -943,7 +943,7 @@
943943 }
944944 },
945945 "node_modules/@paperclover/console": {
946 "resolved": "git+https://git.paperclover.net/clo/console.git#29ae0fe6bb948723655b2d875faef75433b78ac0",
946 "resolved": "git+https://git.paperclover.net/clo/console.git#acdad1b233b5e27dd8ff817d3b8ee7f1a88519d3",
947947 "dependencies": {
948948 "ansi-escapes": "^7.0.0",
949949 "chalk": "^5.4.1",
package.json-1
......@@ -18,7 +18,6 @@
1818 "#backend": "./src/backend.ts",
1919 "#sitegen": "./framework/lib/sitegen.ts",
2020 "#sitegen/*": "./framework/lib/*.ts",
21 "#sqlite": "./framework/sqlite.ts",
2221 "#ssr": "./framework/engine/ssr.ts",
2322 "#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts",
2423 "#ssr/jsx-runtime": "./framework/engine/jsx-runtime.ts",
readme.md+41
......@@ -13,7 +13,48 @@ that assist building websites. these tools power https://paperclover.net.
1313 - The only difference in development and production mode is hidden
1414 source-maps and stripped assertions and `console.debug` calls. The site
1515 you see locally is the site you see deployed.
16- Tools for building complex, content heavy web sites.
17 - Static asset serving with ETag and build-time compression.
18 - Dynamic server side rendering from within backend code.
19 - Databases with a typed SQLite wrapper.
1620- Built on the battle-tested Node.js runtime. Partial support for Deno and Bun.
1721
1822[1]: https://next.markojs.com
1923
24Included is `src`, which contains `paperclover.net`. Website highlights:
25
26- [Question/Answer board, custom markdown parser and components][q+a].
27- [File viewer with prefetching + client-side navigation][file].
28- [Personal, friends-only blog with password protection][friends].
29
30[q+a]: https://paperclover.net/q+a
31[file]: https://paperclover.net/file
32[friends]: https://paperclover.net/friends
33
34## Development
35
36```
37npm install
38
39# production generation
40node run generate
41
42# live development environment
43node repl
44```
45
46`repl.js` will open a read-eval-print-loop where plugin state is cached (on my
472014 dev laptop, startup time is 600-1000ms). every file in `framework` and
48`src` besides `hot.ts` can be edited and quickly re-run. for example, to run
49`framework/generate.ts`, you can type "generate" into the shell. since
50top-level await is not supported (plugins are built on `require` as Node has
51poor module support), CLIs can include a `main` function, which is executed
52when the REPL runs it.
53
54TODO: in the future, the repl will have a "dev" command which will do the incremental
55site build + host, possibly immediatly on opening the repl.
56
57## Contributions
58
59No contributions to `src` accepted, only `framework`.
60
src/admin.ts+1-1
......@@ -59,6 +59,6 @@ export function hasAdminToken(c: Context) {
5959 return token && compareToken(token);
6060}
6161
62import * as fs from "fs";
62import * as fs from "node:fs";
6363import type { Context, Next } from "hono";
6464import { serveAsset } from "#sitegen/assets";
src/backend.ts+1-1
......@@ -30,7 +30,7 @@ async function removeDuplicateSlashes(c: Context, next: Next) {
3030 await next();
3131}
3232
33import { Hono } from "#hono";
33import { type Context, Hono, type Next } from "#hono";
3434import { logger } from "hono/logger";
3535import { trimTrailingSlash } from "hono/trailing-slash";
3636import * as assets from "#sitegen/assets";
src/q+a/models/PendingQuestion.ts+1-1
......@@ -66,7 +66,7 @@ const getByDateQuery = db.prepare<[qmid: number]>(`
6666 SELECT * FROM questions WHERE qmid = ? AND type = ${QuestionType.pending} LIMIT 1
6767`).as(PendingQuestion);
6868
69import { getDb } from "#sqlite";
69import { getDb } from "#sitegen/sqlite";
7070import assert from "node:assert";
7171import { QuestionType } from "./Question.ts";
7272import { formatQuestionId } from "../format.ts";
src/q+a/models/Question.ts+1-1
......@@ -115,4 +115,4 @@ const updateByQmidQuery = db.prepare<
115115`);
116116
117117import { formatQuestionId } from "../format.ts";
118import { getDb } from "#sqlite";
118import { getDb } from "#sitegen/sqlite";