primative backend support
This commit is contained in:
parent
46a67453a1
commit
0c5db556f1
21 changed files with 217 additions and 57 deletions
32
framework/backend/entry-node.ts
Normal file
32
framework/backend/entry-node.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import "@paperclover/console/inject";
|
||||||
|
|
||||||
|
const protocol = "http";
|
||||||
|
|
||||||
|
const server = serve(app, ({ address, port }) => {
|
||||||
|
if (address === "::") address = "::1";
|
||||||
|
console.info(url.format({
|
||||||
|
protocol,
|
||||||
|
hostname: address,
|
||||||
|
port,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
import app from "#backend";
|
||||||
|
import url from "node:url";
|
||||||
|
import { serve } from "@hono/node-server";
|
||||||
|
import process from "node:process";
|
|
@ -49,7 +49,7 @@ export async function bundleClientJavaScript(
|
||||||
const publicScriptRoutes = extraPublicScripts.map((file) =>
|
const publicScriptRoutes = extraPublicScripts.map((file) =>
|
||||||
path.basename(file).replace(/\.client\.[tj]sx?/, "")
|
path.basename(file).replace(/\.client\.[tj]sx?/, "")
|
||||||
);
|
);
|
||||||
const promises: Promise<unknown>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
// TODO: add a shared build hash to entrypoints, derived from all the chunk hashes.
|
// TODO: add a shared build hash to entrypoints, derived from all the chunk hashes.
|
||||||
for (const file of bundle.outputFiles) {
|
for (const file of bundle.outputFiles) {
|
||||||
let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
|
let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
|
||||||
|
@ -73,10 +73,23 @@ export async function bundleClientJavaScript(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
if (promises.length > 0) {
|
export async function bundleServerJavaScript(entryPoint: string) {
|
||||||
await Promise.all(promises);
|
const bundle = await esbuild.build({
|
||||||
}
|
bundle: true,
|
||||||
|
chunkNames: "/js/c.[hash]",
|
||||||
|
entryNames: "/js/[name]",
|
||||||
|
assetNames: "/asset/[hash]",
|
||||||
|
entryPoints: [entryPoint],
|
||||||
|
format: "esm",
|
||||||
|
minify: true,
|
||||||
|
outdir: "/out!",
|
||||||
|
plugins,
|
||||||
|
splitting: true,
|
||||||
|
write: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function writeMkdirSync(file: string, contents: Buffer | string) {
|
||||||
|
|
||||||
export function readDirRecOptionalSync(dir: string) {
|
export function readDirRecOptionalSync(dir: string) {
|
||||||
try {
|
try {
|
||||||
return readdirSync(dir, { withFileTypes: true });
|
return readdirSync(dir, { recursive: true, withFileTypes: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code === "ENOENT") return [];
|
if (err.code === "ENOENT") return [];
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// Sitegen! Clover's static site generator, built with love.
|
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
return withSpinner({
|
return withSpinner({
|
||||||
text: "Recovering State",
|
text: "Recovering State",
|
||||||
|
@ -26,8 +24,8 @@ async function sitegen(status: Spinner) {
|
||||||
const incr = new Incremental();
|
const incr = new Incremental();
|
||||||
|
|
||||||
// Sitegen reviews every defined section for resources to process
|
// Sitegen reviews every defined section for resources to process
|
||||||
const sections: Section[] =
|
const sections: sg.Section[] =
|
||||||
require(path.join(root, "sections.ts")).siteSections;
|
require(path.join(root, "site.ts")).siteSections;
|
||||||
|
|
||||||
// Static files are compressed and served as-is.
|
// Static files are compressed and served as-is.
|
||||||
// - "{section}/static/*.png"
|
// - "{section}/static/*.png"
|
||||||
|
@ -43,8 +41,6 @@ async function sitegen(status: Spinner) {
|
||||||
// Note that '.client.ts' can be placed anywhere in the file structure.
|
// Note that '.client.ts' can be placed anywhere in the file structure.
|
||||||
// - "{section}/scripts/*.client.ts"
|
// - "{section}/scripts/*.client.ts"
|
||||||
let scripts: FileItem[] = [];
|
let scripts: FileItem[] = [];
|
||||||
// 'backend.ts'
|
|
||||||
const backendFiles = [];
|
|
||||||
|
|
||||||
// -- Scan for files --
|
// -- Scan for files --
|
||||||
status.text = "Scanning Project";
|
status.text = "Scanning Project";
|
||||||
|
@ -88,11 +84,6 @@ async function sitegen(status: Spinner) {
|
||||||
list.push({ id, file: path.join(item.parentPath, item.name) });
|
list.push({ id, file: path.join(item.parentPath, item.name) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let backendFile = [
|
|
||||||
sectionPath("backend.ts"),
|
|
||||||
sectionPath("backend.tsx"),
|
|
||||||
].find((file) => fs.existsSync(file));
|
|
||||||
if (backendFile) backendFiles.push(backendFile);
|
|
||||||
}
|
}
|
||||||
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
|
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
|
||||||
const globalCssPath = join("global.css");
|
const globalCssPath = join("global.css");
|
||||||
|
@ -215,7 +206,7 @@ async function sitegen(status: Spinner) {
|
||||||
// -- copy/compress static files --
|
// -- copy/compress static files --
|
||||||
async function doStaticFile(item: FileItem) {
|
async function doStaticFile(item: FileItem) {
|
||||||
const body = await fs.readFile(item.file);
|
const body = await fs.readFile(item.file);
|
||||||
incr.putAsset({
|
await incr.putAsset({
|
||||||
srcId: "static:" + item.file,
|
srcId: "static:" + item.file,
|
||||||
key: item.id,
|
key: item.id,
|
||||||
body,
|
body,
|
||||||
|
@ -250,7 +241,7 @@ async function sitegen(status: Spinner) {
|
||||||
),
|
),
|
||||||
).map((x) => `{${x}}`).join("\n"),
|
).map((x) => `{${x}}`).join("\n"),
|
||||||
});
|
});
|
||||||
incr.putAsset({
|
await incr.putAsset({
|
||||||
srcId: "page:" + page.file,
|
srcId: "page:" + page.file,
|
||||||
key: page.id,
|
key: page.id,
|
||||||
body: doc,
|
body: doc,
|
||||||
|
@ -295,7 +286,6 @@ function wrapDocument({
|
||||||
}</head><body>${body}${scripts ? `<script>${scripts}</script>` : ""}</body>`;
|
}</head><body>${body}${scripts ? `<script>${scripts}</script>` : ""}</body>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { Section } from "./sitegen-lib.ts";
|
|
||||||
import { OnceMap, Queue } from "./queue.ts";
|
import { OnceMap, Queue } from "./queue.ts";
|
||||||
import { Incremental } from "./incremental.ts";
|
import { Incremental } from "./incremental.ts";
|
||||||
import * as bundle from "./bundle.ts";
|
import * as bundle from "./bundle.ts";
|
||||||
|
@ -304,6 +294,6 @@ import * as fs from "./fs.ts";
|
||||||
import { Spinner, withSpinner } from "@paperclover/console/Spinner";
|
import { Spinner, withSpinner } from "@paperclover/console/Spinner";
|
||||||
import * as meta from "./meta.ts";
|
import * as meta from "./meta.ts";
|
||||||
import * as ssr from "./engine/ssr.ts";
|
import * as ssr from "./engine/ssr.ts";
|
||||||
import * as sg from "./sitegen-lib.ts";
|
import * as sg from "#sitegen";
|
||||||
import * as hot from "./hot.ts";
|
import * as hot from "./hot.ts";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
|
@ -146,6 +146,7 @@ function loadEsbuildCode(module: NodeJS.Module, filepath: string, src: string) {
|
||||||
target: "esnext",
|
target: "esnext",
|
||||||
jsx: "automatic",
|
jsx: "automatic",
|
||||||
jsxImportSource: "#ssr",
|
jsxImportSource: "#ssr",
|
||||||
|
sourcefile: filepath,
|
||||||
}).code;
|
}).code;
|
||||||
return module._compile(src, filepath, "commonjs");
|
return module._compile(src, filepath, "commonjs");
|
||||||
}
|
}
|
||||||
|
@ -162,8 +163,6 @@ function loadMarko(module: NodeJS.Module, filepath: string) {
|
||||||
) + '\nimport { Script as CloverScriptInclude } from "#sitegen";\n';
|
) + '\nimport { Script as CloverScriptInclude } from "#sitegen";\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(src);
|
|
||||||
console.log("---");
|
|
||||||
src = marko.compileSync(src, filepath).code;
|
src = marko.compileSync(src, filepath).code;
|
||||||
src = src.replace("marko/debug/html", "#ssr/marko");
|
src = src.replace("marko/debug/html", "#ssr/marko");
|
||||||
return loadEsbuildCode(module, filepath, src);
|
return loadEsbuildCode(module, filepath, src);
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
// all its imports) to map to the build of a page, which produces an HTML file
|
// all its imports) to map to the build of a page, which produces an HTML file
|
||||||
// plus a list of scripts.
|
// plus a list of scripts.
|
||||||
|
|
||||||
import { Buffer } from "node:buffer";
|
|
||||||
interface ArtifactMap {
|
interface ArtifactMap {
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
script: string;
|
script: string;
|
||||||
|
@ -126,18 +125,17 @@ export class Incremental {
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
|
const a = this.put({ ...info, type: "asset", value });
|
||||||
if (!this.compress.has(hash)) {
|
if (!this.compress.has(hash)) {
|
||||||
const label = info.key;
|
const label = info.key;
|
||||||
this.compress.set(hash, {
|
this.compress.set(hash, {
|
||||||
zstd: undefined,
|
zstd: undefined,
|
||||||
gzip: undefined,
|
gzip: undefined,
|
||||||
});
|
});
|
||||||
await Promise.all([
|
this.compressQueue.add({ label, buffer, algo: "zstd", hash });
|
||||||
this.compressQueue.add({ label, buffer, algo: "zstd", hash }),
|
this.compressQueue.add({ label, buffer, algo: "gzip", hash });
|
||||||
this.compressQueue.add({ label, buffer, algo: "gzip", hash }),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
return this.put({ ...info, type: "asset", value });
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
async compressImpl({ algo, buffer, hash }: CompressJob) {
|
async compressImpl({ algo, buffer, hash }: CompressJob) {
|
||||||
|
@ -167,16 +165,18 @@ export class Incremental {
|
||||||
}
|
}
|
||||||
|
|
||||||
async wait() {
|
async wait() {
|
||||||
await this.compressQueue.done({ method: "stop" });
|
await this.compressQueue.done({ method: "success" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async flush() {
|
async flush() {
|
||||||
|
ASSERT(!this.compressQueue.active);
|
||||||
const writer = new BufferWriter();
|
const writer = new BufferWriter();
|
||||||
const asset = Object.fromEntries(
|
const asset = Object.fromEntries(
|
||||||
Array.from(this.out.asset, ([key, { buffer, hash, headers }]) => {
|
Array.from(this.out.asset, ([key, { buffer, hash, headers }]) => {
|
||||||
const raw = writer.write(buffer, hash);
|
const raw = writer.write(buffer, hash);
|
||||||
const { gzip: gzipBuf, zstd: zstdBuf } = this.compress.get(hash) ?? {};
|
const { gzip: gzipBuf, zstd: zstdBuf } = this.compress.get(hash) ?? {};
|
||||||
const gzip = gzipBuf ? writer.write(gzipBuf, hash) : null;
|
const gzip = gzipBuf ? writer.write(gzipBuf, hash + ".gz") : null;
|
||||||
const zstd = zstdBuf ? writer.write(zstdBuf, hash) : null;
|
const zstd = zstdBuf ? writer.write(zstdBuf, hash + ".zstd") : null;
|
||||||
return [key, {
|
return [key, {
|
||||||
raw,
|
raw,
|
||||||
gzip,
|
gzip,
|
||||||
|
@ -252,10 +252,6 @@ export interface SerializedMeta {
|
||||||
script: [key: string, value: string][];
|
script: [key: string, value: string][];
|
||||||
}
|
}
|
||||||
|
|
||||||
function never(): never {
|
|
||||||
throw new Error("Impossible");
|
|
||||||
}
|
|
||||||
|
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as fs from "./fs.ts";
|
import * as fs from "./fs.ts";
|
||||||
import * as zlib from "node:zlib";
|
import * as zlib from "node:zlib";
|
||||||
|
@ -263,3 +259,4 @@ import * as util from "node:util";
|
||||||
import { Queue } from "./queue.ts";
|
import { Queue } from "./queue.ts";
|
||||||
import * as hot from "./hot.ts";
|
import * as hot from "./hot.ts";
|
||||||
import * as mime from "./mime.ts";
|
import * as mime from "./mime.ts";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
|
|
@ -26,7 +26,7 @@ export async function reloadSync() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function assetMiddleware(c: Context, next: Next) {
|
export async function middleware(c: Context, next: Next) {
|
||||||
if (!assets) await reload();
|
if (!assets) await reload();
|
||||||
const asset = assets!.map[c.req.path];
|
const asset = assets!.map[c.req.path];
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
@ -35,6 +35,19 @@ export async function assetMiddleware(c: Context, next: Next) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function notFound(c: Context) {
|
||||||
|
if (!assets) await reload();
|
||||||
|
let pathname = c.req.path;
|
||||||
|
do {
|
||||||
|
const asset = assets!.map[pathname + "/404"];
|
||||||
|
if (asset) return assetInner(c, asset, 404);
|
||||||
|
pathname = pathname.slice(0, pathname.lastIndexOf("/"));
|
||||||
|
} while (pathname);
|
||||||
|
const asset = assets!.map["/404"];
|
||||||
|
if (asset) return assetInner(c, asset, 404);
|
||||||
|
return c.text("the 'Not Found' page was not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
export async function serveAsset(
|
export async function serveAsset(
|
||||||
c: Context,
|
c: Context,
|
||||||
id: StaticPageId,
|
id: StaticPageId,
|
||||||
|
@ -62,14 +75,13 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
|
||||||
if (ifnonematch) {
|
if (ifnonematch) {
|
||||||
const etag = asset.headers.ETag;
|
const etag = asset.headers.ETag;
|
||||||
if (etagMatches(etag, ifnonematch)) {
|
if (etagMatches(etag, ifnonematch)) {
|
||||||
c.res = new Response(null, {
|
return c.res = new Response(null, {
|
||||||
status: 304,
|
status: 304,
|
||||||
statusText: "Not Modified",
|
statusText: "Not Modified",
|
||||||
headers: {
|
headers: {
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const acceptEncoding = c.req.header("Accept-Encoding") ?? "";
|
const acceptEncoding = c.req.header("Accept-Encoding") ?? "";
|
||||||
|
@ -90,11 +102,11 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
|
||||||
} else {
|
} else {
|
||||||
body = subarrayAsset(asset.raw);
|
body = subarrayAsset(asset.raw);
|
||||||
}
|
}
|
||||||
c.res = new Response(body, { headers, status });
|
return c.res = new Response(body, { headers, status });
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as fs from "./fs.ts";
|
import * as fs from "../fs.ts";
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import type { StatusCode } from "hono/utils/http-status";
|
import type { StatusCode } from "hono/utils/http-status";
|
||||||
import type { BuiltAsset, BuiltAssetMap, View } from "./incremental.ts";
|
import type { BuiltAsset, BuiltAssetMap, View } from "../incremental.ts";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
|
@ -1,6 +1,8 @@
|
||||||
// Import this file with 'import * as sg from "#sitegen";'
|
// Import this file with 'import * as sg from "#sitegen";'
|
||||||
export type ScriptId = string;
|
export type ScriptId = string;
|
||||||
|
|
||||||
|
const frameworkDir = path.dirname(import.meta.dirname);
|
||||||
|
|
||||||
export interface SitegenRender {
|
export interface SitegenRender {
|
||||||
scripts: Set<ScriptId>;
|
scripts: Set<ScriptId>;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +24,7 @@ export function getRender() {
|
||||||
/** Add a client-side script to the page. */
|
/** Add a client-side script to the page. */
|
||||||
export function addScript(id: ScriptId) {
|
export function addScript(id: ScriptId) {
|
||||||
const srcFile: string = util.getCallSites()
|
const srcFile: string = util.getCallSites()
|
||||||
.find((site) => !site.scriptName.startsWith(import.meta.dirname))!
|
.find((site) => !site.scriptName.startsWith(frameworkDir))!
|
||||||
.scriptName;
|
.scriptName;
|
||||||
const filePath = hot.resolveFrom(srcFile, id);
|
const filePath = hot.resolveFrom(srcFile, id);
|
||||||
if (
|
if (
|
||||||
|
@ -44,6 +46,7 @@ export interface Section {
|
||||||
root: string;
|
root: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as ssr from "./engine/ssr.ts";
|
import * as ssr from "../engine/ssr.ts";
|
||||||
import * as util from "node:util";
|
import * as util from "node:util";
|
||||||
import * as hot from "./hot.ts";
|
import * as hot from "../hot.ts";
|
||||||
|
import * as path from "node:path";
|
|
@ -165,6 +165,10 @@ export class Queue<T, R> {
|
||||||
|
|
||||||
if (bar) bar[method]();
|
if (bar) bar[method]();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get active(): boolean {
|
||||||
|
return this.#active.length !== 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
|
@ -41,11 +41,8 @@ export class WrappedDatabase {
|
||||||
(key, version) values (?, ?);
|
(key, version) values (?, ?);
|
||||||
`),
|
`),
|
||||||
));
|
));
|
||||||
const { changes, lastInsertRowid } = s.run(name, 1);
|
const { changes } = s.run(name, 1);
|
||||||
console.log(changes, lastInsertRowid);
|
if (changes === 1) this.node.exec(schema);
|
||||||
if (changes === 1) {
|
|
||||||
this.node.exec(schema);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare<Args extends unknown[] = [], Result = unknown>(
|
prepare<Args extends unknown[] = [], Result = unknown>(
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#sitegen": "./framework/sitegen-lib.ts",
|
"#backend": "./src/backend.ts",
|
||||||
|
"#sitegen": "./framework/lib/sitegen.ts",
|
||||||
|
"#sitegen/*": "./framework/lib/*.ts",
|
||||||
"#sqlite": "./framework/sqlite.ts",
|
"#sqlite": "./framework/sqlite.ts",
|
||||||
"#ssr": "./framework/engine/ssr.ts",
|
"#ssr": "./framework/engine/ssr.ts",
|
||||||
"#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts",
|
"#ssr/jsx-dev-runtime": "./framework/engine/jsx-runtime.ts",
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
"production": "marko/production",
|
"production": "marko/production",
|
||||||
"node": "marko/debug/html"
|
"node": "marko/debug/html"
|
||||||
},
|
},
|
||||||
|
"#hono": "hono",
|
||||||
"#hono/platform": {
|
"#hono/platform": {
|
||||||
"bun": "hono/bun",
|
"bun": "hono/bun",
|
||||||
"deno": "hono/deno",
|
"deno": "hono/deno",
|
||||||
|
|
1
readme.md
Normal file
1
readme.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# clover sitegen framework
|
7
repl.js
7
repl.js
|
@ -27,8 +27,7 @@ hot.load("node:repl").start({
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hot.reloadRecursive("./framework/engine/ssr.ts");
|
hot.reloadRecursive("./framework/generate.tsx");
|
||||||
hot.reloadRecursive("./framework/bundle.ts");
|
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
async function evaluate(code) {
|
async function evaluate(code) {
|
||||||
|
@ -41,11 +40,11 @@ async function evaluate(code) {
|
||||||
if (code[0] === "=") {
|
if (code[0] === "=") {
|
||||||
try {
|
try {
|
||||||
const result = await eval(code[1]);
|
const result = await eval(code[1]);
|
||||||
console.log(inspect(result));
|
console.info(inspect(result));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof SyntaxError) {
|
if (err instanceof SyntaxError) {
|
||||||
const result = await eval("(async() => { return " + code + " })()");
|
const result = await eval("(async() => { return " + code + " })()");
|
||||||
console.log(inspect(result));
|
console.info(inspect(result));
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
2
run.js
2
run.js
|
@ -18,7 +18,7 @@ import process from "node:process";
|
||||||
const hot = await import("./framework/hot.ts");
|
const hot = await import("./framework/hot.ts");
|
||||||
|
|
||||||
const console = hot.load("@paperclover/console");
|
const console = hot.load("@paperclover/console");
|
||||||
globalThis.console.log = console.info;
|
globalThis.console["log"] = console.info;
|
||||||
globalThis.console.info = console.info;
|
globalThis.console.info = console.info;
|
||||||
globalThis.console.warn = console.warn;
|
globalThis.console.warn = console.warn;
|
||||||
globalThis.console.error = console.error;
|
globalThis.console.error = console.error;
|
||||||
|
|
64
src/admin.ts
Normal file
64
src/admin.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
const cookieAge = 60 * 60 * 24 * 365; // 1 year
|
||||||
|
|
||||||
|
let lastKnownToken: string | null = null;
|
||||||
|
function compareToken(token: string) {
|
||||||
|
if (token === lastKnownToken) return true;
|
||||||
|
lastKnownToken = fs.readFileSync(".clover/admin-token.txt", "utf8").trim();
|
||||||
|
return token === lastKnownToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function middleware(c: Context, next: Next) {
|
||||||
|
if (c.req.path.startsWith("/admin")) {
|
||||||
|
return adminInner(c, next);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function adminInner(c: Context, next: Next) {
|
||||||
|
const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
|
||||||
|
|
||||||
|
if (c.req.path === "/admin/login") {
|
||||||
|
const key = c.req.query("key");
|
||||||
|
if (key) {
|
||||||
|
if (compareToken(key)) {
|
||||||
|
return c.body(null, 303, {
|
||||||
|
"Location": "/admin",
|
||||||
|
"Set-Cookie":
|
||||||
|
`admin-token=${key}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return serveAsset(c, "/admin/login/fail", 403);
|
||||||
|
}
|
||||||
|
if (token && compareToken(token)) {
|
||||||
|
return c.redirect("/admin", 303);
|
||||||
|
}
|
||||||
|
if (c.req.method === "POST") {
|
||||||
|
return serveAsset(c, "/admin/login/fail", 403);
|
||||||
|
} else {
|
||||||
|
return serveAsset(c, "/admin/login", 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.req.path === "/admin/logout") {
|
||||||
|
return c.body(null, 303, {
|
||||||
|
"Location": "/admin/login",
|
||||||
|
"Set-Cookie":
|
||||||
|
`admin-token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && compareToken(token)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.redirect("/admin/login", 303);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasAdminToken(c: Context) {
|
||||||
|
const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
|
||||||
|
return token && compareToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
import * as fs from "fs";
|
||||||
|
import type { Context, Next } from "hono";
|
||||||
|
import { serveAsset } from "#sitegen/assets";
|
38
src/backend.ts
Normal file
38
src/backend.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const logHttp = scoped("http", { color: "magenta" });
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.notFound(assets.notFound);
|
||||||
|
|
||||||
|
app.use(trimTrailingSlash());
|
||||||
|
app.use(removeDuplicateSlashes);
|
||||||
|
app.use(logger((msg) => msg.startsWith("-->") && logHttp(msg.slice(4))));
|
||||||
|
app.use(admin.middleware);
|
||||||
|
|
||||||
|
app.route("", require("./q+a/backend.ts").app);
|
||||||
|
// app.route("", require("./file-viewer/backend.tsx").app);
|
||||||
|
// app.route("", require("./friends/backend.tsx").app);
|
||||||
|
|
||||||
|
app.use(assets.middleware);
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
||||||
|
async function removeDuplicateSlashes(c: Context, next: Next) {
|
||||||
|
const path = c.req.path;
|
||||||
|
if (/\/\/+/.test(path)) {
|
||||||
|
const normalizedPath = path.replace(/\/\/+/g, "/");
|
||||||
|
const query = c.req.query();
|
||||||
|
const queryString = Object.keys(query).length > 0
|
||||||
|
? "?" + new URLSearchParams(query).toString()
|
||||||
|
: "";
|
||||||
|
return c.redirect(normalizedPath + queryString, 301);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
import { Hono } from "#hono";
|
||||||
|
import { logger } from "hono/logger";
|
||||||
|
import { trimTrailingSlash } from "hono/trailing-slash";
|
||||||
|
import * as assets from "#sitegen/assets";
|
||||||
|
import * as admin from "./admin.ts";
|
||||||
|
import { scoped } from "@paperclover/console";
|
6
src/pages/404.mdx
Normal file
6
src/pages/404.mdx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export const meta = { title: 'oh no,,,' };
|
||||||
|
|
||||||
|
# oh dear
|
||||||
|
|
||||||
|
sound the alarms
|
||||||
|
|
|
@ -16,7 +16,6 @@ export const meta: Meta = {
|
||||||
type: "website",
|
type: "website",
|
||||||
url: "https://paperclover.net",
|
url: "https://paperclover.net",
|
||||||
},
|
},
|
||||||
generator: "clover",
|
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: "https://paperclover.net",
|
canonical: "https://paperclover.net",
|
||||||
types: {
|
types: {
|
||||||
|
|
3
src/q+a/backend.ts
Normal file
3
src/q+a/backend.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const app = new Hono();
|
||||||
|
|
||||||
|
import { Hono } from "#hono";
|
|
@ -30,7 +30,7 @@ static const transitionDate = 1735639200000;
|
||||||
</>
|
</>
|
||||||
|
|
||||||
// this singleton script will make all the '<time>' tags clickable.
|
// this singleton script will make all the '<time>' tags clickable.
|
||||||
client import "./clickable-links.ts";
|
client import "./clickable-links.client.ts";
|
||||||
|
|
||||||
import type { Question } from "@/q+a/models/Question.ts";
|
import type { Question } from "@/q+a/models/Question.ts";
|
||||||
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
|
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
|
||||||
|
|
Loading…
Reference in a new issue