| author | |
| committer | |
| log | ad50869c177bec4aa2eb956e55acbee3ba04ad27 |
| tree | 205b5fb1ccac84685e481d51a4bc3d7ceebb303b |
| parent | 8421676693001c607e4963009031d205fa94bbe6 |
| signature | Commit is signed but in an unrecognized format. |
24 files changed, 372 insertions(+), 331 deletions(-)
framework/backend/entry-passthru.ts created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | import "@paperclover/console/inject"; | |
| 2 | export default app; | |
| 3 | ||
| 4 | import app from "#backend"; |
framework/css.ts+2-3| ... | ... | @@ -64,7 +64,7 @@ export async function bundleCssFiles( |
| 64 | 64 | }), |
| 65 | 65 | ); |
| 66 | 66 | }, |
| 67 | } satisfies Plugin; | |
| 67 | } satisfies esbuild.Plugin; | |
| 68 | 68 | const build = await esbuild.build({ |
| 69 | 69 | bundle: true, |
| 70 | 70 | entryPoints: ["$input$"], |
| ... | ... | @@ -85,6 +85,5 @@ export async function bundleCssFiles( |
| 85 | 85 | return outputFiles[0].text; |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | import type { Plugin } from "esbuild"; | |
| 89 | 88 | import * as esbuild from "esbuild"; |
| 90 | import * as fs from "./fs.ts"; | |
| 89 | import * 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. | |
| 3 | export { | |
| 4 | existsSync, | |
| 5 | readdir, | |
| 6 | readdirSync, | |
| 7 | readFile, | |
| 8 | readFileSync, | |
| 9 | rm, | |
| 10 | rmSync, | |
| 11 | stat, | |
| 12 | statSync, | |
| 13 | writeFile, | |
| 14 | writeFileSync, | |
| 15 | }; | |
| 16 | ||
| 17 | export function mkdir(dir: string) { | |
| 18 | return nodeMkdir(dir, { recursive: true }); | |
| 19 | } | |
| 20 | ||
| 21 | export function mkdirSync(dir: string) { | |
| 22 | return nodeMkdirSync(dir, { recursive: true }); | |
| 23 | } | |
| 24 | ||
| 25 | export async function writeMkdir(file: string, contents: Buffer | string) { | |
| 26 | await mkdir(path.dirname(file)); | |
| 27 | return writeFile(file, contents); | |
| 28 | } | |
| 29 | ||
| 30 | export function writeMkdirSync(file: string, contents: Buffer | string) { | |
| 31 | mkdirSync(path.dirname(file)); | |
| 32 | return writeFileSync(file, contents); | |
| 33 | } | |
| 34 | ||
| 35 | export 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 | ||
| 44 | import * as path from "node:path"; | |
| 45 | import { | |
| 46 | existsSync, | |
| 47 | mkdirSync as nodeMkdirSync, | |
| 48 | readdirSync, | |
| 49 | readFileSync, | |
| 50 | rmSync, | |
| 51 | statSync, | |
| 52 | writeFileSync, | |
| 53 | } from "node:fs"; | |
| 54 | import { | |
| 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"; |
| 290 | 290 | import { Incremental } from "./incremental.ts"; |
| 291 | 291 | import * as bundle from "./bundle.ts"; |
| 292 | 292 | import * as css from "./css.ts"; |
| 293 | import * as fs from "./fs.ts"; | |
| 294 | import { Spinner, withSpinner } from "@paperclover/console/Spinner"; | |
| 295 | import * as meta from "./meta.ts"; | |
| 296 | 293 | import * as ssr from "./engine/ssr.ts"; |
| 297 | import * as sg from "#sitegen"; | |
| 298 | 294 | import * as hot from "./hot.ts"; |
| 295 | import * as fs from "#sitegen/fs"; | |
| 296 | import * as sg from "#sitegen"; | |
| 299 | 297 | import * as path from "node:path"; |
| 298 | import * as meta from "#sitegen/meta"; | |
| 299 | import { Spinner, withSpinner } from "@paperclover/console/Spinner"; |
framework/hot.ts+1-1| ... | ... | @@ -236,7 +236,7 @@ declare module "node:module" { |
| 236 | 236 | ): unknown; |
| 237 | 237 | } |
| 238 | 238 | |
| 239 | import * as fs from "./fs.ts"; | |
| 239 | import * as fs from "./lib/fs.ts"; | |
| 240 | 240 | import * as path from "node:path"; |
| 241 | 241 | import { pathToFileURL } from "node:url"; |
| 242 | 242 | import * as esbuild from "esbuild"; |
framework/incremental.ts+2-3| ... | ... | @@ -252,11 +252,10 @@ export interface SerializedMeta { |
| 252 | 252 | script: [key: string, value: string][]; |
| 253 | 253 | } |
| 254 | 254 | |
| 255 | import * as path from "node:path"; | |
| 256 | import * as fs from "./fs.ts"; | |
| 255 | import * as fs from "#sitegen/fs"; | |
| 257 | 256 | import * as zlib from "node:zlib"; |
| 258 | 257 | import * as util from "node:util"; |
| 259 | 258 | import { Queue } from "./queue.ts"; |
| 260 | 259 | import * as hot from "./hot.ts"; |
| 261 | import * as mime from "./mime.ts"; | |
| 260 | import * as mime from "#sitegen/mime"; | |
| 262 | 261 | import { Buffer } from "node:buffer"; |
framework/lib/assets.ts+1-1| ... | ... | @@ -105,7 +105,7 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) { |
| 105 | 105 | return c.res = new Response(body, { headers, status }); |
| 106 | 106 | } |
| 107 | 107 | |
| 108 | import * as fs from "../fs.ts"; | |
| 108 | import * as fs from "#sitegen/fs"; | |
| 109 | 109 | import type { Context, Next } from "hono"; |
| 110 | 110 | import type { StatusCode } from "hono/utils/http-status"; |
| 111 | 111 | import 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. | |
| 3 | export { | |
| 4 | existsSync, | |
| 5 | readdir, | |
| 6 | readdirSync, | |
| 7 | readFile, | |
| 8 | readFileSync, | |
| 9 | rm, | |
| 10 | rmSync, | |
| 11 | stat, | |
| 12 | statSync, | |
| 13 | writeFile, | |
| 14 | writeFileSync, | |
| 15 | }; | |
| 16 | ||
| 17 | export function mkdir(dir: string) { | |
| 18 | return nodeMkdir(dir, { recursive: true }); | |
| 19 | } | |
| 20 | ||
| 21 | export function mkdirSync(dir: string) { | |
| 22 | return nodeMkdirSync(dir, { recursive: true }); | |
| 23 | } | |
| 24 | ||
| 25 | export async function writeMkdir(file: string, contents: Buffer | string) { | |
| 26 | await mkdir(path.dirname(file)); | |
| 27 | return writeFile(file, contents); | |
| 28 | } | |
| 29 | ||
| 30 | export function writeMkdirSync(file: string, contents: Buffer | string) { | |
| 31 | mkdirSync(path.dirname(file)); | |
| 32 | return writeFileSync(file, contents); | |
| 33 | } | |
| 34 | ||
| 35 | export 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 | ||
| 44 | import * as path from "node:path"; | |
| 45 | import { | |
| 46 | existsSync, | |
| 47 | mkdirSync as nodeMkdirSync, | |
| 48 | readdirSync, | |
| 49 | readFileSync, | |
| 50 | rmSync, | |
| 51 | statSync, | |
| 52 | writeFileSync, | |
| 53 | } from "node:fs"; | |
| 54 | import { | |
| 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 @@ |
| 1 | export interface Meta { | |
| 2 | title: string; | |
| 3 | description?: string | undefined; | |
| 4 | openGraph?: OpenGraph; | |
| 5 | alternates?: Alternates; | |
| 6 | } | |
| 7 | export interface OpenGraph { | |
| 8 | title?: string; | |
| 9 | description?: string | undefined; | |
| 10 | type: string; | |
| 11 | url: string; | |
| 12 | } | |
| 13 | export interface Alternates { | |
| 14 | canonical: string; | |
| 15 | types: { [mime: string]: AlternateType }; | |
| 16 | } | |
| 17 | export interface AlternateType { | |
| 18 | url: string; | |
| 19 | title: string; | |
| 20 | } | |
| 21 | export function renderMeta({ title }: Meta): string { | |
| 22 | return `<title>${esc(title)}</title>`; | |
| 23 | } | |
| 24 | import { escapeHtml as esc } from "../engine/ssr.ts"; |
framework/lib/mime.ts created+31| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | const 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]); | |
| 9 | const extensions = new Map(entries.filter((x) => x[0].startsWith("."))); | |
| 10 | const 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 | */ | |
| 17 | export 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 | ||
| 30 | import * as fs from "#sitegen/fs"; | |
| 31 | import * 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 | |
| 99 | rss.xml application/rss+xml |
framework/lib/sqlite.ts created+97| ... | ... | @@ -0,0 +1,97 @@ |
| 1 | // Guard against reloads and bundler duplication. | |
| 2 | // @ts-ignore | |
| 3 | const map = globalThis[Symbol.for("clover.db")] ??= new Map< | |
| 4 | string, | |
| 5 | WrappedDatabase | |
| 6 | >(); | |
| 7 | ||
| 8 | export 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 | ||
| 19 | export 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 | ||
| 55 | export 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 | ||
| 96 | import { DatabaseSync, StatementSync } from "node:sqlite"; | |
| 97 | import * as path from "node:path"; |
framework/meta.ts deleted-24| ... | ... | @@ -1,24 +0,0 @@ |
| 1 | export interface Meta { | |
| 2 | title: string; | |
| 3 | description?: string | undefined; | |
| 4 | openGraph?: OpenGraph; | |
| 5 | alternates?: Alternates; | |
| 6 | } | |
| 7 | export interface OpenGraph { | |
| 8 | title?: string; | |
| 9 | description?: string | undefined; | |
| 10 | type: string; | |
| 11 | url: string; | |
| 12 | } | |
| 13 | export interface Alternates { | |
| 14 | canonical: string; | |
| 15 | types: { [mime: string]: AlternateType }; | |
| 16 | } | |
| 17 | export interface AlternateType { | |
| 18 | url: string; | |
| 19 | title: string; | |
| 20 | } | |
| 21 | export function renderMeta({ title }: Meta): string { | |
| 22 | return `<title>${esc(title)}</title>`; | |
| 23 | } | |
| 24 | import { escapeHtml as esc } from "./engine/ssr.ts"; |
framework/mime.ts deleted-31| ... | ... | @@ -1,31 +0,0 @@ |
| 1 | const 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]); | |
| 9 | const extensions = new Map(entries.filter((x) => x[0].startsWith("."))); | |
| 10 | const 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 | */ | |
| 17 | export 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 | ||
| 30 | import * as fs from "./fs.ts"; | |
| 31 | import * 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 | |
| 99 | rss.xml application/rss+xml |
framework/sqlite.ts deleted-98| ... | ... | @@ -1,98 +0,0 @@ |
| 1 | // Guard against reloads and bundler duplication. | |
| 2 | // @ts-ignore | |
| 3 | const map = globalThis[Symbol.for("clover.db")] ??= new Map< | |
| 4 | string, | |
| 5 | WrappedDatabase | |
| 6 | >(); | |
| 7 | ||
| 8 | export 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 | ||
| 19 | export 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 | ||
| 55 | export 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 | ||
| 96 | import { DatabaseSync, StatementSync } from "node:sqlite"; | |
| 97 | import * as fs from "./fs.ts"; | |
| 98 | import * as path from "node:path"; |
framework/typecheck.ts deletedpackage-lock.json+1-1| ... | ... | @@ -943,7 +943,7 @@ |
| 943 | 943 | } |
| 944 | 944 | }, |
| 945 | 945 | "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", | |
| 947 | 947 | "dependencies": { |
| 948 | 948 | "ansi-escapes": "^7.0.0", |
| 949 | 949 | "chalk": "^5.4.1", |
package.json-1| ... | ... | @@ -18,7 +18,6 @@ |
| 18 | 18 | "#backend": "./src/backend.ts", |
| 19 | 19 | "#sitegen": "./framework/lib/sitegen.ts", |
| 20 | 20 | "#sitegen/*": "./framework/lib/*.ts", |
| 21 | "#sqlite": "./framework/sqlite.ts", | |
| 22 | 21 | "#ssr": "./framework/engine/ssr.ts", |
| 23 | 22 | "#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts", |
| 24 | 23 | "#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. |
| 13 | 13 | - The only difference in development and production mode is hidden |
| 14 | 14 | source-maps and stripped assertions and `console.debug` calls. The site |
| 15 | 15 | 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. | |
| 16 | 20 | - Built on the battle-tested Node.js runtime. Partial support for Deno and Bun. |
| 17 | 21 | |
| 18 | 22 | [1]: https://next.markojs.com |
| 19 | 23 | |
| 24 | Included 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 | ``` | |
| 37 | npm install | |
| 38 | ||
| 39 | # production generation | |
| 40 | node run generate | |
| 41 | ||
| 42 | # live development environment | |
| 43 | node repl | |
| 44 | ``` | |
| 45 | ||
| 46 | `repl.js` will open a read-eval-print-loop where plugin state is cached (on my | |
| 47 | 2014 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 | |
| 50 | top-level await is not supported (plugins are built on `require` as Node has | |
| 51 | poor module support), CLIs can include a `main` function, which is executed | |
| 52 | when the REPL runs it. | |
| 53 | ||
| 54 | TODO: in the future, the repl will have a "dev" command which will do the incremental | |
| 55 | site build + host, possibly immediatly on opening the repl. | |
| 56 | ||
| 57 | ## Contributions | |
| 58 | ||
| 59 | No contributions to `src` accepted, only `framework`. | |
| 60 |
src/admin.ts+1-1| ... | ... | @@ -59,6 +59,6 @@ export function hasAdminToken(c: Context) { |
| 59 | 59 | return token && compareToken(token); |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | import * as fs from "fs"; | |
| 62 | import * as fs from "node:fs"; | |
| 63 | 63 | import type { Context, Next } from "hono"; |
| 64 | 64 | import { serveAsset } from "#sitegen/assets"; |
src/backend.ts+1-1| ... | ... | @@ -30,7 +30,7 @@ async function removeDuplicateSlashes(c: Context, next: Next) { |
| 30 | 30 | await next(); |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | import { Hono } from "#hono"; | |
| 33 | import { type Context, Hono, type Next } from "#hono"; | |
| 34 | 34 | import { logger } from "hono/logger"; |
| 35 | 35 | import { trimTrailingSlash } from "hono/trailing-slash"; |
| 36 | 36 | import * as assets from "#sitegen/assets"; |
src/q+a/models/PendingQuestion.ts+1-1| ... | ... | @@ -66,7 +66,7 @@ const getByDateQuery = db.prepare<[qmid: number]>(` |
| 66 | 66 | SELECT * FROM questions WHERE qmid = ? AND type = ${QuestionType.pending} LIMIT 1 |
| 67 | 67 | `).as(PendingQuestion); |
| 68 | 68 | |
| 69 | import { getDb } from "#sqlite"; | |
| 69 | import { getDb } from "#sitegen/sqlite"; | |
| 70 | 70 | import assert from "node:assert"; |
| 71 | 71 | import { QuestionType } from "./Question.ts"; |
| 72 | 72 | import { formatQuestionId } from "../format.ts"; |
src/q+a/models/Question.ts+1-1| ... | ... | @@ -115,4 +115,4 @@ const updateByQmidQuery = db.prepare< |
| 115 | 115 | `); |
| 116 | 116 | |
| 117 | 117 | import { formatQuestionId } from "../format.ts"; |
| 118 | import { getDb } from "#sqlite"; | |
| 118 | import { getDb } from "#sitegen/sqlite"; |