diff --git a/framework/backend/entry-passthru.ts b/framework/backend/entry-passthru.ts new file mode 100644 index 0000000000000000000000000000000000000000..1273b673008e9e7d4fcf886035252ce5f4e2176a --- /dev/null +++ b/framework/backend/entry-passthru.ts @@ -0,0 +1,4 @@ +import "@paperclover/console/inject"; +export default app; + +import app from "#backend"; diff --git a/framework/css.ts b/framework/css.ts index c7063c523d659e1f4cb8ce82281ced7e35e3f7dc..bc7ba8f647c2310105c0e41b6b0b8200e7c3b7dc 100644 --- a/framework/css.ts +++ b/framework/css.ts @@ -64,7 +64,7 @@ export async function bundleCssFiles( }), ); }, - } satisfies Plugin; + } satisfies esbuild.Plugin; const build = await esbuild.build({ bundle: true, entryPoints: ["$input$"], @@ -85,6 +85,5 @@ export async function bundleCssFiles( return outputFiles[0].text; } -import type { Plugin } from "esbuild"; import * as esbuild from "esbuild"; -import * as fs from "./fs.ts"; +import * as fs from "#sitegen/fs"; diff --git a/framework/fs.ts b/framework/fs.ts deleted file mode 100644 index 4761f37f8d31b262d723161853230dc0e29946d7..0000000000000000000000000000000000000000 --- a/framework/fs.ts +++ /dev/null @@ -1,61 +0,0 @@ -// File System APIs. Some custom APIs, but mostly a re-export a mix of built-in -// Node.js sync+promise fs methods. For convenince. -export { - existsSync, - readdir, - readdirSync, - readFile, - readFileSync, - rm, - rmSync, - stat, - statSync, - writeFile, - writeFileSync, -}; - -export function mkdir(dir: string) { - return nodeMkdir(dir, { recursive: true }); -} - -export function mkdirSync(dir: string) { - return nodeMkdirSync(dir, { recursive: true }); -} - -export async function writeMkdir(file: string, contents: Buffer | string) { - await mkdir(path.dirname(file)); - return writeFile(file, contents); -} - -export function writeMkdirSync(file: string, contents: Buffer | string) { - mkdirSync(path.dirname(file)); - return writeFileSync(file, contents); -} - -export function readDirRecOptionalSync(dir: string) { - try { - return readdirSync(dir, { recursive: true, withFileTypes: true }); - } catch (err: any) { - if (err.code === "ENOENT") return []; - throw err; - } -} - -import * as path from "node:path"; -import { - existsSync, - mkdirSync as nodeMkdirSync, - readdirSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { - mkdir as nodeMkdir, - readdir, - readFile, - rm, - stat, - writeFile, -} from "node:fs/promises"; diff --git a/framework/generate.tsx b/framework/generate.tsx index cbe24f7a1c6b98288d3615b2c87acb80995ecf8f..69825f29ca3251b3f83d55178ce962df03261423 100644 --- a/framework/generate.tsx +++ b/framework/generate.tsx @@ -290,10 +290,10 @@ import { OnceMap, Queue } from "./queue.ts"; import { Incremental } from "./incremental.ts"; import * as bundle from "./bundle.ts"; import * as css from "./css.ts"; -import * as fs from "./fs.ts"; -import { Spinner, withSpinner } from "@paperclover/console/Spinner"; -import * as meta from "./meta.ts"; import * as ssr from "./engine/ssr.ts"; -import * as sg from "#sitegen"; import * as hot from "./hot.ts"; +import * as fs from "#sitegen/fs"; +import * as sg from "#sitegen"; import * as path from "node:path"; +import * as meta from "#sitegen/meta"; +import { Spinner, withSpinner } from "@paperclover/console/Spinner"; diff --git a/framework/hot.ts b/framework/hot.ts index 8e60aacff161b89d9eab8f868dfa744d64d86b15..1f612c4078a626df5645b51572317a39b3d89599 100644 --- a/framework/hot.ts +++ b/framework/hot.ts @@ -236,7 +236,7 @@ declare module "node:module" { ): unknown; } -import * as fs from "./fs.ts"; +import * as fs from "./lib/fs.ts"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import * as esbuild from "esbuild"; diff --git a/framework/incremental.ts b/framework/incremental.ts index 793cf07dd1967602128fac66faad4a44e1c41d4d..d77f6762fb812809cc4059bcd1a06fa021e8b38c 100644 --- a/framework/incremental.ts +++ b/framework/incremental.ts @@ -252,11 +252,10 @@ export interface SerializedMeta { script: [key: string, value: string][]; } -import * as path from "node:path"; -import * as fs from "./fs.ts"; +import * as fs from "#sitegen/fs"; import * as zlib from "node:zlib"; import * as util from "node:util"; import { Queue } from "./queue.ts"; import * as hot from "./hot.ts"; -import * as mime from "./mime.ts"; +import * as mime from "#sitegen/mime"; import { Buffer } from "node:buffer"; diff --git a/framework/lib/assets.ts b/framework/lib/assets.ts index 9dc295bb040c0cd6c9bec141a22b481a23c7150a..751571f87d1db018546f4bf9b070e1d551c3304d 100644 --- a/framework/lib/assets.ts +++ b/framework/lib/assets.ts @@ -105,7 +105,7 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) { return c.res = new Response(body, { headers, status }); } -import * as fs from "../fs.ts"; +import * as fs from "#sitegen/fs"; import type { Context, Next } from "hono"; import type { StatusCode } from "hono/utils/http-status"; import type { BuiltAsset, BuiltAssetMap, View } from "../incremental.ts"; diff --git a/framework/lib/fs.ts b/framework/lib/fs.ts new file mode 100644 index 0000000000000000000000000000000000000000..4761f37f8d31b262d723161853230dc0e29946d7 --- /dev/null +++ b/framework/lib/fs.ts @@ -0,0 +1,61 @@ +// File System APIs. Some custom APIs, but mostly a re-export a mix of built-in +// Node.js sync+promise fs methods. For convenince. +export { + existsSync, + readdir, + readdirSync, + readFile, + readFileSync, + rm, + rmSync, + stat, + statSync, + writeFile, + writeFileSync, +}; + +export function mkdir(dir: string) { + return nodeMkdir(dir, { recursive: true }); +} + +export function mkdirSync(dir: string) { + return nodeMkdirSync(dir, { recursive: true }); +} + +export async function writeMkdir(file: string, contents: Buffer | string) { + await mkdir(path.dirname(file)); + return writeFile(file, contents); +} + +export function writeMkdirSync(file: string, contents: Buffer | string) { + mkdirSync(path.dirname(file)); + return writeFileSync(file, contents); +} + +export function readDirRecOptionalSync(dir: string) { + try { + return readdirSync(dir, { recursive: true, withFileTypes: true }); + } catch (err: any) { + if (err.code === "ENOENT") return []; + throw err; + } +} + +import * as path from "node:path"; +import { + existsSync, + mkdirSync as nodeMkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { + mkdir as nodeMkdir, + readdir, + readFile, + rm, + stat, + writeFile, +} from "node:fs/promises"; diff --git a/framework/lib/meta.ts b/framework/lib/meta.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0b65a977dd5209877fbcae58195ec2be35f594c --- /dev/null +++ b/framework/lib/meta.ts @@ -0,0 +1,24 @@ +export interface Meta { + title: string; + description?: string | undefined; + openGraph?: OpenGraph; + alternates?: Alternates; +} +export interface OpenGraph { + title?: string; + description?: string | undefined; + type: string; + url: string; +} +export interface Alternates { + canonical: string; + types: { [mime: string]: AlternateType }; +} +export interface AlternateType { + url: string; + title: string; +} +export function renderMeta({ title }: Meta): string { + return `${esc(title)}`; +} +import { escapeHtml as esc } from "../engine/ssr.ts"; diff --git a/framework/lib/mime.ts b/framework/lib/mime.ts new file mode 100644 index 0000000000000000000000000000000000000000..3560a8c29b68069fe858f21b39515473c97fe7be --- /dev/null +++ b/framework/lib/mime.ts @@ -0,0 +1,31 @@ +const entries = fs.readFileSync( + path.join(import.meta.dirname, "mime.txt"), + "utf8", +) + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .map((line) => line.split(/\s+/, 2) as [string, string]); +const extensions = new Map(entries.filter((x) => x[0].startsWith("."))); +const fullNames = new Map(entries.filter((x) => !x[0].startsWith("."))); + +/** + * Accepts: + * - Full file path or basename + * - Extension (with or without dot) + */ +export function contentTypeFor(file: string) { + const slash = file.indexOf("/"); + if (slash !== -1) file = file.slice(slash + 1); + const dot = file.indexOf("."); + if (dot === -1) file = "." + file; + else if (dot > 0) { + let entry = fullNames.get(file); + if (entry) return entry; + file = file.slice(dot); + } + return extensions.get(file) ?? "application/octet-stream"; +} + +import * as fs from "#sitegen/fs"; +import * as path from "node:path"; diff --git a/framework/lib/mime.txt b/framework/lib/mime.txt new file mode 100644 index 0000000000000000000000000000000000000000..7cf59936e371a7a55e468ab37eb0725fbf92e11e --- /dev/null +++ b/framework/lib/mime.txt @@ -0,0 +1,99 @@ +# media types +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types +.aac audio/x-aac +.aif audio/x-aiff +.aifc audio/x-aiff +.aiff audio/x-aiff +.asm text/x-asm +.avi video/x-msvideo +.bat application/x-msdownload +.c text/x-c +.chat text/x-clover-chatlog +.class application/java-vm +.cmd application/x-msdownload +.com application/x-msdownload +.conf text/plain +.cpp text/x-c +.css text/css +.csv text/csv +.cxx text/x-c +.def text/plain +.diff text/plain +.dll application/x-msdownload +.dmg application/octet-stream +.doc application/msword +.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document +.epub application/epub+zip +.exe application/x-msdownload +.flv video/x-flv +.fbx application/fbx +.gz application/x-gzip +.h text/x-c +.h264 video/h264 +.hh text/x-c +.htm text/html;charset=utf-8 +.html text/html;charset=utf-8 +.ico image/x-icon +.ics text/calendar +.in text/plain +.jar application/java-archive +.java text/x-java-source +.jpeg image/jpeg +.jpg image/jpeg +.jpgv video/jpeg +.jxl image/jxl +.js application/javascript +.json application/json +.latex application/x-latex +.list text/plain +.log text/plain +.m4a audio/mp4 +.man text/troff +.mid audio/midi +.midi audio/midi +.mov video/quicktime +.mp3 audio/mpeg +.mp4 video/mp4 +.msh model/mesh +.msi application/x-msdownload +.obj application/octet-stream +.ogg audio/ogg +.otf application/x-font-otf +.pdf application/pdf +.png image/png +.ppt application/vnd.ms-powerpoint +.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation +.psd image/vnd.adobe.photoshop +.py text/x-python +.rar application/x-rar-compressed +.rss application/rss+xml +.rtf application/rtf +.rtx text/richtext +.s text/x-asm +.pem application/x-pem-file" +.ser application/java-serialized-object +.sh application/x-sh +.sig application/pgp-signature +.silo model/mesh +.svg image/svg+xml +.t text/troff +.tar application/x-tar +.text text/plain +.tgz application/x-gzip +.tif image/tiff +.tiff image/tiff +.torrent application/x-bittorrent +.ttc application/x-font-ttf +.ttf application/x-font-ttf +.txt text/plain +.urls text/uri-list +.v text/x-v +.wav audio/x-wav +.wmv video/x-ms-wmv +.xls application/vnd.ms-excel +.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +.xml application/xml +.xps application/vnd.ms-xpsdocument + +# special cased based on file name +rss.xml application/rss+xml diff --git a/framework/lib/sqlite.ts b/framework/lib/sqlite.ts new file mode 100644 index 0000000000000000000000000000000000000000..4018c18741c3d76643448e32372557da502b4dc5 --- /dev/null +++ b/framework/lib/sqlite.ts @@ -0,0 +1,97 @@ +// Guard against reloads and bundler duplication. +// @ts-ignore +const map = globalThis[Symbol.for("clover.db")] ??= new Map< + string, + WrappedDatabase +>(); + +export function getDb(file: string) { + let db: WrappedDatabase | null = map.get(file); + if (db) return db; + const fileWithExt = file.includes(".") ? file : file + ".sqlite"; + db = new WrappedDatabase( + new DatabaseSync(path.join(".clover/", fileWithExt)), + ); + map.set(file, db); + return db; +} + +export class WrappedDatabase { + node: DatabaseSync; + stmtTableMigrate: WeakRef | null = null; + + constructor(node: DatabaseSync) { + this.node = node; + this.node.exec(` + create table if not exists clover_migrations ( + key text not null primary key, + version integer not null + ); + `); + } + + // TODO: add migration support + // the idea is you keep `schema` as the new schema but can add + // migrations to the mix really easily. + table(name: string, schema: string) { + let s = this.stmtTableMigrate?.deref(); + s ?? (this.stmtTableMigrate = new WeakRef( + s = this.node.prepare(` + insert or ignore into clover_migrations + (key, version) values (?, ?); + `), + )); + const { changes } = s.run(name, 1); + if (changes === 1) this.node.exec(schema); + } + + prepare( + query: string, + ): Stmt { + return new Stmt(this.node.prepare(query)); + } +} + +export class Stmt { + #node: StatementSync; + #class: any | null = null; + constructor(node: StatementSync) { + this.#node = node; + } + + /** Get one row */ + get(...args: Args): Row | null { + const item = this.#node.get(...args as any) as Row; + if (!item) return null; + const C = this.#class; + if (C) Object.setPrototypeOf(item, C.prototype); + return item; + } + getNonNull(...args: Args) { + const item = this.get(...args); + if (!item) throw new Error("Query returned no result"); + return item; + } + iter(...args: Args): Iterator { + return this.array(...args)[Symbol.iterator](); + } + /** Get all rows */ + array(...args: Args): Row[] { + const array = this.#node.all(...args as any) as Row[]; + const C = this.#class; + if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype)); + return array; + } + /** Return the number of changes / row ID */ + run(...args: Args) { + return this.#node.run(...args as any); + } + + as(Class: { new (): R }): Stmt { + this.#class = Class; + return this as any; + } +} + +import { DatabaseSync, StatementSync } from "node:sqlite"; +import * as path from "node:path"; diff --git a/framework/meta.ts b/framework/meta.ts deleted file mode 100644 index cfde26b31c77d52ada00b7344464aefe1192a0db..0000000000000000000000000000000000000000 --- a/framework/meta.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface Meta { - title: string; - description?: string | undefined; - openGraph?: OpenGraph; - alternates?: Alternates; -} -export interface OpenGraph { - title?: string; - description?: string | undefined; - type: string; - url: string; -} -export interface Alternates { - canonical: string; - types: { [mime: string]: AlternateType }; -} -export interface AlternateType { - url: string; - title: string; -} -export function renderMeta({ title }: Meta): string { - return `${esc(title)}`; -} -import { escapeHtml as esc } from "./engine/ssr.ts"; diff --git a/framework/mime.ts b/framework/mime.ts deleted file mode 100644 index d71480b931e9716a12434f911b41da5a2940a4a8..0000000000000000000000000000000000000000 --- a/framework/mime.ts +++ /dev/null @@ -1,31 +0,0 @@ -const entries = fs.readFileSync( - path.join(import.meta.dirname, "mime.txt"), - "utf8", -) - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")) - .map((line) => line.split(/\s+/, 2) as [string, string]); -const extensions = new Map(entries.filter((x) => x[0].startsWith("."))); -const fullNames = new Map(entries.filter((x) => !x[0].startsWith("."))); - -/** - * Accepts: - * - Full file path or basename - * - Extension (with or without dot) - */ -export function contentTypeFor(file: string) { - const slash = file.indexOf("/"); - if (slash !== -1) file = file.slice(slash + 1); - const dot = file.indexOf("."); - if (dot === -1) file = "." + file; - else if (dot > 0) { - let entry = fullNames.get(file); - if (entry) return entry; - file = file.slice(dot); - } - return extensions.get(file) ?? "application/octet-stream"; -} - -import * as fs from "./fs.ts"; -import * as path from "node:path"; diff --git a/framework/mime.txt b/framework/mime.txt deleted file mode 100644 index 7cf59936e371a7a55e468ab37eb0725fbf92e11e..0000000000000000000000000000000000000000 --- a/framework/mime.txt +++ /dev/null @@ -1,99 +0,0 @@ -# media types -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types -.aac audio/x-aac -.aif audio/x-aiff -.aifc audio/x-aiff -.aiff audio/x-aiff -.asm text/x-asm -.avi video/x-msvideo -.bat application/x-msdownload -.c text/x-c -.chat text/x-clover-chatlog -.class application/java-vm -.cmd application/x-msdownload -.com application/x-msdownload -.conf text/plain -.cpp text/x-c -.css text/css -.csv text/csv -.cxx text/x-c -.def text/plain -.diff text/plain -.dll application/x-msdownload -.dmg application/octet-stream -.doc application/msword -.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document -.epub application/epub+zip -.exe application/x-msdownload -.flv video/x-flv -.fbx application/fbx -.gz application/x-gzip -.h text/x-c -.h264 video/h264 -.hh text/x-c -.htm text/html;charset=utf-8 -.html text/html;charset=utf-8 -.ico image/x-icon -.ics text/calendar -.in text/plain -.jar application/java-archive -.java text/x-java-source -.jpeg image/jpeg -.jpg image/jpeg -.jpgv video/jpeg -.jxl image/jxl -.js application/javascript -.json application/json -.latex application/x-latex -.list text/plain -.log text/plain -.m4a audio/mp4 -.man text/troff -.mid audio/midi -.midi audio/midi -.mov video/quicktime -.mp3 audio/mpeg -.mp4 video/mp4 -.msh model/mesh -.msi application/x-msdownload -.obj application/octet-stream -.ogg audio/ogg -.otf application/x-font-otf -.pdf application/pdf -.png image/png -.ppt application/vnd.ms-powerpoint -.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation -.psd image/vnd.adobe.photoshop -.py text/x-python -.rar application/x-rar-compressed -.rss application/rss+xml -.rtf application/rtf -.rtx text/richtext -.s text/x-asm -.pem application/x-pem-file" -.ser application/java-serialized-object -.sh application/x-sh -.sig application/pgp-signature -.silo model/mesh -.svg image/svg+xml -.t text/troff -.tar application/x-tar -.text text/plain -.tgz application/x-gzip -.tif image/tiff -.tiff image/tiff -.torrent application/x-bittorrent -.ttc application/x-font-ttf -.ttf application/x-font-ttf -.txt text/plain -.urls text/uri-list -.v text/x-v -.wav audio/x-wav -.wmv video/x-ms-wmv -.xls application/vnd.ms-excel -.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -.xml application/xml -.xps application/vnd.ms-xpsdocument - -# special cased based on file name -rss.xml application/rss+xml diff --git a/framework/sqlite.ts b/framework/sqlite.ts deleted file mode 100644 index 1646932f64c99e59c25c24614ece6a81f72a194d..0000000000000000000000000000000000000000 --- a/framework/sqlite.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Guard against reloads and bundler duplication. -// @ts-ignore -const map = globalThis[Symbol.for("clover.db")] ??= new Map< - string, - WrappedDatabase ->(); - -export function getDb(file: string) { - let db: WrappedDatabase | null = map.get(file); - if (db) return db; - const fileWithExt = file.includes(".") ? file : file + ".sqlite"; - db = new WrappedDatabase( - new DatabaseSync(path.join(".clover/", fileWithExt)), - ); - map.set(file, db); - return db; -} - -export class WrappedDatabase { - node: DatabaseSync; - stmtTableMigrate: WeakRef | null = null; - - constructor(node: DatabaseSync) { - this.node = node; - this.node.exec(` - create table if not exists clover_migrations ( - key text not null primary key, - version integer not null - ); - `); - } - - // TODO: add migration support - // the idea is you keep `schema` as the new schema but can add - // migrations to the mix really easily. - table(name: string, schema: string) { - let s = this.stmtTableMigrate?.deref(); - s ?? (this.stmtTableMigrate = new WeakRef( - s = this.node.prepare(` - insert or ignore into clover_migrations - (key, version) values (?, ?); - `), - )); - const { changes } = s.run(name, 1); - if (changes === 1) this.node.exec(schema); - } - - prepare( - query: string, - ): Stmt { - return new Stmt(this.node.prepare(query)); - } -} - -export class Stmt { - #node: StatementSync; - #class: any | null = null; - constructor(node: StatementSync) { - this.#node = node; - } - - /** Get one row */ - get(...args: Args): Row | null { - const item = this.#node.get(...args as any) as Row; - if (!item) return null; - const C = this.#class; - if (C) Object.setPrototypeOf(item, C.prototype); - return item; - } - getNonNull(...args: Args) { - const item = this.get(...args); - if (!item) throw new Error("Query returned no result"); - return item; - } - iter(...args: Args): Iterator { - return this.array(...args)[Symbol.iterator](); - } - /** Get all rows */ - array(...args: Args): Row[] { - const array = this.#node.all(...args as any) as Row[]; - const C = this.#class; - if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype)); - return array; - } - /** Return the number of changes / row ID */ - run(...args: Args) { - return this.#node.run(...args as any); - } - - as(Class: { new (): R }): Stmt { - this.#class = Class; - return this as any; - } -} - -import { DatabaseSync, StatementSync } from "node:sqlite"; -import * as fs from "./fs.ts"; -import * as path from "node:path"; diff --git a/framework/typecheck.ts b/framework/typecheck.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/package-lock.json b/package-lock.json index 805fb0344b53ef18eb404e4e8f1d7e7e10e0ebe9..214569439dc8d9c4d0bc2ab65f99243f37f02986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -943,7 +943,7 @@ } }, "node_modules/@paperclover/console": { - "resolved": "git+https://git.paperclover.net/clo/console.git#29ae0fe6bb948723655b2d875faef75433b78ac0", + "resolved": "git+https://git.paperclover.net/clo/console.git#acdad1b233b5e27dd8ff817d3b8ee7f1a88519d3", "dependencies": { "ansi-escapes": "^7.0.0", "chalk": "^5.4.1", diff --git a/package.json b/package.json index ef8de70c34aea6e9962c5aed9dbae4b131d3c943..76abb02be19c37d5793123e1abb9b21ed4902eed 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "#backend": "./src/backend.ts", "#sitegen": "./framework/lib/sitegen.ts", "#sitegen/*": "./framework/lib/*.ts", - "#sqlite": "./framework/sqlite.ts", "#ssr": "./framework/engine/ssr.ts", "#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts", "#ssr/jsx-runtime": "./framework/engine/jsx-runtime.ts", diff --git a/readme.md b/readme.md index 533018db6669d78935b16cdaf3a596038fe108f4..37bd508908acc582cb927aedd983e1531dabddf7 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,48 @@ that assist building websites. these tools power https://paperclover.net. - The only difference in development and production mode is hidden source-maps and stripped assertions and `console.debug` calls. The site you see locally is the site you see deployed. +- Tools for building complex, content heavy web sites. + - Static asset serving with ETag and build-time compression. + - Dynamic server side rendering from within backend code. + - Databases with a typed SQLite wrapper. - Built on the battle-tested Node.js runtime. Partial support for Deno and Bun. [1]: https://next.markojs.com +Included is `src`, which contains `paperclover.net`. Website highlights: + +- [Question/Answer board, custom markdown parser and components][q+a]. +- [File viewer with prefetching + client-side navigation][file]. +- [Personal, friends-only blog with password protection][friends]. + +[q+a]: https://paperclover.net/q+a +[file]: https://paperclover.net/file +[friends]: https://paperclover.net/friends + +## Development + +``` +npm install + +# production generation +node run generate + +# live development environment +node repl +``` + +`repl.js` will open a read-eval-print-loop where plugin state is cached (on my +2014 dev laptop, startup time is 600-1000ms). every file in `framework` and +`src` besides `hot.ts` can be edited and quickly re-run. for example, to run +`framework/generate.ts`, you can type "generate" into the shell. since +top-level await is not supported (plugins are built on `require` as Node has +poor module support), CLIs can include a `main` function, which is executed +when the REPL runs it. + +TODO: in the future, the repl will have a "dev" command which will do the incremental +site build + host, possibly immediatly on opening the repl. + +## Contributions + +No contributions to `src` accepted, only `framework`. + diff --git a/src/admin.ts b/src/admin.ts index 96691e6a59aea94e0436bee5afeae5a156bdada3..3e9c3943a87af1db9b146b9125f6924e5519ec89 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -59,6 +59,6 @@ export function hasAdminToken(c: Context) { return token && compareToken(token); } -import * as fs from "fs"; +import * as fs from "node:fs"; import type { Context, Next } from "hono"; import { serveAsset } from "#sitegen/assets"; diff --git a/src/backend.ts b/src/backend.ts index 5cbda382711d713315caedd0a1ab1fec29bb68a3..3e6c20fee5870a9daa9ba18cd580222cc695ce4c 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -30,7 +30,7 @@ async function removeDuplicateSlashes(c: Context, next: Next) { await next(); } -import { Hono } from "#hono"; +import { type Context, Hono, type Next } from "#hono"; import { logger } from "hono/logger"; import { trimTrailingSlash } from "hono/trailing-slash"; import * as assets from "#sitegen/assets"; diff --git a/src/q+a/models/PendingQuestion.ts b/src/q+a/models/PendingQuestion.ts index f1a239ac6434571fcdae581c989909060a89cebf..4dea9a7fa9942bbd7d7a4aab0210eff0bb6ffd40 100644 --- a/src/q+a/models/PendingQuestion.ts +++ b/src/q+a/models/PendingQuestion.ts @@ -66,7 +66,7 @@ const getByDateQuery = db.prepare<[qmid: number]>(` SELECT * FROM questions WHERE qmid = ? AND type = ${QuestionType.pending} LIMIT 1 `).as(PendingQuestion); -import { getDb } from "#sqlite"; +import { getDb } from "#sitegen/sqlite"; import assert from "node:assert"; import { QuestionType } from "./Question.ts"; import { formatQuestionId } from "../format.ts"; diff --git a/src/q+a/models/Question.ts b/src/q+a/models/Question.ts index 9f4299415783c5297a3730d500dfe5b4a7e7b67b..d382955cd2b2fff787c7514cc0b305a20b10faa4 100644 --- a/src/q+a/models/Question.ts +++ b/src/q+a/models/Question.ts @@ -115,4 +115,4 @@ const updateByQmidQuery = db.prepare< `); import { formatQuestionId } from "../format.ts"; -import { getDb } from "#sqlite"; +import { getDb } from "#sitegen/sqlite";