finish q+a

This commit is contained in:
chloe caruso 2025-06-15 13:11:21 -07:00
parent db244583d7
commit 7f5011bace
20 changed files with 259 additions and 68 deletions

View file

@ -23,7 +23,8 @@ export async function bundleClientJavaScript(
} }
const clientPlugins: esbuild.Plugin[] = [ const clientPlugins: esbuild.Plugin[] = [
// There are currently no plugins needed by 'paperclover.net' projectRelativeResolution(),
markoViaBuildCache(incr),
]; ];
const bundle = await esbuild.build({ const bundle = await esbuild.build({
@ -42,6 +43,9 @@ export async function bundleClientJavaScript(
jsx: "automatic", jsx: "automatic",
jsxImportSource: "#ssr", jsxImportSource: "#ssr",
jsxDev: dev, jsxDev: dev,
define: {
"ASSERT": "console.assert",
},
}); });
if (bundle.errors.length || bundle.warnings.length) { if (bundle.errors.length || bundle.warnings.length) {
throw new AggregateError( throw new AggregateError(
@ -50,11 +54,15 @@ export async function bundleClientJavaScript(
); );
} }
const publicScriptRoutes = extraPublicScripts.map((file) => const publicScriptRoutes = extraPublicScripts.map((file) =>
path.basename(file).replace(/\.client\.[tj]sx?/, "") "/js/" +
path.relative(hot.projectSrc, file).replaceAll("\\", "/").replace(
/\.client\.[tj]sx?/,
".js",
)
); );
const { metafile } = bundle; const { metafile, outputFiles } = bundle;
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
for (const file of bundle.outputFiles) { for (const file of outputFiles) {
const { text } = file; const { text } = file;
let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/"); let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
const { inputs } = UNWRAP(metafile.outputs["out!" + route]); const { inputs } = UNWRAP(metafile.outputs["out!" + route]);
@ -63,8 +71,8 @@ export async function bundleClientJavaScript(
// Register non-chunks as script entries. // Register non-chunks as script entries.
const chunk = route.startsWith("/js/c."); const chunk = route.startsWith("/js/c.");
if (!chunk) { if (!chunk) {
const key = hot.getScriptId(path.resolve(sources[0])); const key = hot.getScriptId(path.resolve(sources[sources.length - 1]));
route = "/js/" + key + ".js"; route = "/js/" + key.replace(/\.client\.tsx?/, ".js");
incr.put({ incr.put({
sources, sources,
kind: "script", kind: "script",
@ -123,27 +131,7 @@ export async function bundleServerJavaScript(
"$views": viewSource, "$views": viewSource,
}), }),
projectRelativeResolution(), projectRelativeResolution(),
{ markoViaBuildCache(incr),
name: "marko via build cache",
setup(b) {
b.onLoad(
{ filter: /\.marko$/ },
async ({ path: file }) => {
const key = path.relative(hot.projectRoot, file)
.replaceAll("\\", "/");
const cacheEntry = incr.out.serverMarko.get(key);
if (!cacheEntry) {
throw new Error("Marko file not in cache: " + file);
}
return ({
loader: "ts",
contents: cacheEntry.src,
resolveDir: path.dirname(file),
});
},
);
},
},
{ {
name: "replace client references", name: "replace client references",
setup(b) { setup(b) {
@ -168,6 +156,9 @@ export async function bundleServerJavaScript(
}, },
}, },
]; ];
const pkg = await fs.readJson("package.json") as {
dependencies: Record<string, string>;
};
const { metafile, outputFiles } = await esbuild.build({ const { metafile, outputFiles } = await esbuild.build({
bundle: true, bundle: true,
chunkNames: "c.[hash]", chunkNames: "c.[hash]",
@ -186,6 +177,8 @@ export async function bundleServerJavaScript(
jsx: "automatic", jsx: "automatic",
jsxImportSource: "#ssr", jsxImportSource: "#ssr",
jsxDev: false, jsxDev: false,
external: Object.keys(pkg.dependencies)
.filter((x) => !x.startsWith("@paperclover")),
}); });
const files: Record<string, Buffer> = {}; const files: Record<string, Buffer> = {};
@ -279,6 +272,29 @@ export async function finalizeServerJavaScript(
}); });
} }
function markoViaBuildCache(incr: Incremental): esbuild.Plugin {
return {
name: "marko via build cache",
setup(b) {
b.onLoad(
{ filter: /\.marko$/ },
async ({ path: file }) => {
const key = path.relative(hot.projectRoot, file)
.replaceAll("\\", "/");
const cacheEntry = incr.out.serverMarko.get(key);
if (!cacheEntry) {
throw new Error("Marko file not in cache: " + file);
}
return ({
loader: "ts",
contents: cacheEntry.src,
resolveDir: path.dirname(file),
});
},
);
},
};
}
import * as esbuild from "esbuild"; import * as esbuild from "esbuild";
import * as path from "node:path"; import * as path from "node:path";
import process from "node:process"; import process from "node:process";

View file

@ -52,7 +52,6 @@ export const dynamicTag = (
if (typeof tag === "function") { if (typeof tag === "function") {
clover: { clover: {
const unwrapped = (tag as any).unwrapped; const unwrapped = (tag as any).unwrapped;
console.log({ tag, unwrapped });
if (unwrapped) { if (unwrapped) {
tag = unwrapped; tag = unwrapped;
break clover; break clover;
@ -130,7 +129,7 @@ export function escapeXML(input: unknown) {
) { ) {
throw new Error( throw new Error(
`Unexpected object in template placeholder: '` + `Unexpected object in template placeholder: '` +
util.inspect({ name: "clover" }) + "'. " + engine.inspect({ name: "clover" }) + "'. " +
`To emit a literal '[object Object]', use \${String(value)}`, `To emit a literal '[object Object]', use \${String(value)}`,
); );
} }
@ -145,4 +144,3 @@ import * as engine from "./ssr.ts";
import type { ServerRenderer } from "marko/html/template"; import type { ServerRenderer } from "marko/html/template";
import { type Accessor } from "marko/common/types"; import { type Accessor } from "marko/common/types";
import * as marko from "#marko/html"; import * as marko from "#marko/html";
import * as util from "node:util";

View file

@ -8,9 +8,9 @@ export function main() {
successText, successText,
failureText: () => "sitegen FAIL", failureText: () => "sitegen FAIL",
}, async (spinner) => { }, async (spinner) => {
const incr = Incremental.fromDisk(); // const incr = Incremental.fromDisk();
await incr.statAllFiles(); // await incr.statAllFiles();
// const incr = new Incremental(); const incr = new Incremental();
const result = await sitegen(spinner, incr); const result = await sitegen(spinner, incr);
incr.toDisk(); // Allows picking up this state again incr.toDisk(); // Allows picking up this state again
return result; return result;
@ -122,7 +122,6 @@ export async function sitegen(
} }
} }
} }
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
const globalCssPath = join("global.css"); const globalCssPath = join("global.css");
// TODO: make sure that `static` and `pages` does not overlap // TODO: make sure that `static` and `pages` does not overlap
@ -311,8 +310,12 @@ export async function sitegen(
maxJobs: 2, maxJobs: 2,
}); });
viewQueue.addMany(neededViews); viewQueue.addMany(neededViews);
await pageQueue.done({ method: "stop" }); const pageAndViews = [
await viewQueue.done({ method: "stop" }); pageQueue.done({ method: "stop" }),
viewQueue.done({ method: "stop" }),
];
await Promise.allSettled(pageAndViews);
await Promise.all(pageAndViews);
status.format = spinnerFormat; status.format = spinnerFormat;
// -- bundle server javascript (backend and views) -- // -- bundle server javascript (backend and views) --

View file

@ -63,7 +63,7 @@ Module.prototype._compile = function (
if (shouldTrackPath(filename)) { if (shouldTrackPath(filename)) {
const cssImportsMaybe: string[] = []; const cssImportsMaybe: string[] = [];
const imports: string[] = []; const imports: string[] = [];
for (const { filename: file } of this.children) { for (const { filename: file, cloverClientRefs } of this.children) {
if (file.endsWith(".css")) cssImportsMaybe.push(file); if (file.endsWith(".css")) cssImportsMaybe.push(file);
else { else {
const child = fileStats.get(file); const child = fileStats.get(file);
@ -71,6 +71,10 @@ Module.prototype._compile = function (
const { cssImportsRecursive } = child; const { cssImportsRecursive } = child;
if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive); if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive);
imports.push(file); imports.push(file);
if (cloverClientRefs && cloverClientRefs.length > 0) {
(this.cloverClientRefs ??= [])
.push(...cloverClientRefs);
}
} }
} }
fileStats.set(filename, { fileStats.set(filename, {

View file

@ -148,7 +148,7 @@ export class Incremental {
for (const key of map.keys()) { for (const key of map.keys()) {
if (!this.round.referenced.has(`${kind}\0${key}`)) { if (!this.round.referenced.has(`${kind}\0${key}`)) {
unreferenced.push({ kind: kind as ArtifactKind, key }); unreferenced.push({ kind: kind as ArtifactKind, key });
this.out[kind as ArtifactKind].delete(key); // this.out[kind as ArtifactKind].delete(key);
} }
} }
} }

View file

@ -11,7 +11,6 @@ export async function reload() {
fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"), fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),
fs.readFile(path.join(import.meta.dirname, "static.blob")), fs.readFile(path.join(import.meta.dirname, "static.blob")),
]); ]);
console.log("new buffer loaded");
assets = { assets = {
map: JSON.parse(map), map: JSON.parse(map),
buf, buf,
@ -110,7 +109,6 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
} }
process.on("message", (msg: any) => { process.on("message", (msg: any) => {
console.log({ msg });
if (msg?.type === "clover.assets.reload") reload(); if (msg?.type === "clover.assets.reload") reload();
}); });

View file

@ -58,8 +58,8 @@ export async function main() {
successText: generate.successText, successText: generate.successText,
failureText: () => "sitegen FAIL", failureText: () => "sitegen FAIL",
}, async (spinner) => { }, async (spinner) => {
console.log("---"); console.info("---");
console.log( console.info(
"Updated" + "Updated" +
(changed.length === 1 (changed.length === 1
? " " + changed[0] ? " " + changed[0]

152
package-lock.json generated
View file

@ -8,6 +8,8 @@
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0", "@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git", "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"hls.js": "^1.6.5", "hls.js": "^1.6.5",
"hono": "^4.7.11", "hono": "^4.7.11",
@ -404,6 +406,87 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
"integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.37.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
"integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -864,6 +947,30 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@luxass/strip-json-comments": { "node_modules/@luxass/strip-json-comments": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz",
@ -873,6 +980,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@marko/compiler": { "node_modules/@marko/compiler": {
"version": "5.39.21", "version": "5.39.21",
"resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz", "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz",
@ -1458,6 +1571,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/collapse-white-space": { "node_modules/collapse-white-space": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
@ -1537,6 +1665,12 @@
} }
} }
}, },
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -1605,6 +1739,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/devalue": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"license": "MIT"
},
"node_modules/devlop": { "node_modules/devlop": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@ -3748,6 +3888,12 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/style-to-js": { "node_modules/style-to-js": {
"version": "1.1.16", "version": "1.1.16",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
@ -4020,6 +4166,12 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View file

@ -4,6 +4,8 @@
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",
"@mdx-js/mdx": "^3.1.0", "@mdx-js/mdx": "^3.1.0",
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git", "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
"codemirror": "^6.0.1",
"devalue": "^5.1.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.5",
"hls.js": "^1.6.5", "hls.js": "^1.6.5",
"hono": "^4.7.11", "hono": "^4.7.11",

View file

@ -127,9 +127,9 @@ app.get("/q+a/:id", async (c, next) => {
const question = Question.getByDate(timestamp); const question = Question.getByDate(timestamp);
if (!question) return next(); if (!question) return next();
// if (image) { if (image) {
// return getQuestionImage(question, c.req.method === "HEAD"); return getQuestionImage(question, c.req.method === "HEAD");
// } }
return renderView(c, "q+a/permalink", { question }); return renderView(c, "q+a/permalink", { question });
}); });
@ -193,11 +193,6 @@ app.get("/q+a/things/random", async (c) => {
c.res = await renderView(c, "q+a/things-random", {}); c.res = await renderView(c, "q+a/things-random", {});
}); });
// 404
app.get("/q+a/*", async (c) => {
return serveAsset(c, "/q+a/404", 404);
});
async function questionFailure( async function questionFailure(
c: Context, c: Context,
status: ContentfulStatusCode, status: ContentfulStatusCode,
@ -229,5 +224,5 @@ import {
} from "./models/PendingQuestion.ts"; } from "./models/PendingQuestion.ts";
import { Question, QuestionType } from "./models/Question.ts"; import { Question, QuestionType } from "./models/Question.ts";
import { renderView } from "#sitegen/view"; import { renderView } from "#sitegen/view";
// import { getQuestionImage } from "./question_image"; import { getQuestionImage } from "./image.tsx";
import { formatQuestionId, questionIdToTimestamp } from "./format.ts"; import { formatQuestionId, questionIdToTimestamp } from "./format.ts";

View file

@ -211,7 +211,7 @@ function renderNode(node: string | ASTNode | ASTNode[]): any {
if (typeof node === "string") { if (typeof node === "string") {
return node; return node;
} else if (Array.isArray(node)) { } else if (Array.isArray(node)) {
return node.flatMap((item) => renderNode(item)); return node.map((item) => renderNode(item));
} else if (node.type === "text") { } else if (node.type === "text") {
return node.content; return node.content;
} else { } else {

View file

@ -1,4 +1,12 @@
const h1 = document.querySelector("h1")!; const h1 = document.querySelector("h1")!;
const key = "net.paperclover.q+a.header";
const state = localStorage?.getItem(key);
if (state === "detrevni ton") h1.classList.toggle("invert");
h1.addEventListener("click", () => { h1.addEventListener("click", () => {
h1.classList.toggle("invert"); h1.classList.toggle("invert");
localStorage?.setItem(
key,
(localStorage.getItem?.(key) ?? "not inverted")
.split("").reverse().join(""),
);
}); });

View file

@ -5,14 +5,13 @@ const cacheImageDir = path.resolve(".clover/question_images");
const getBrowser = RefCountedExpirable( const getBrowser = RefCountedExpirable(
() => () =>
puppeteer.launch({ puppeteer.launch({
// headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"], args: ["--no-sandbox", "--disable-setuid-sandbox"],
}), }),
(b) => b.close(), (b) => b.close(),
); );
export async function renderQuestionImage(question: Question) { export async function renderQuestionImage(question: Question) {
const html = await renderViewToString("q+a/embed-image", { question }); const html = await renderViewToString("q+a/image-embed", { question });
// this browser session will be reused if multiple images are generated // this browser session will be reused if multiple images are generated
// either at the same time or within a 5-minute time span. the dispose // either at the same time or within a 5-minute time span. the dispose

View file

@ -6,6 +6,9 @@ export const meta: Metadata = {
title: "paper clover q+a", title: "paper clover q+a",
description: "ask clover a question", description: "ask clover a question",
}; };
export const regenerate = {
manual: true,
};
<const/inboxSize = PendingQuestion.getAll().length /> <const/inboxSize = PendingQuestion.getAll().length />
<if=(inboxSize > 0)> <if=(inboxSize > 0)>

View file

@ -4,6 +4,7 @@ export const meta: Metadata = {
}; };
<h2>sound the alarms</h2> <h2>sound the alarms</h2>
<p>this page doesn't exist</p>
<p> <p>
<a href="/q+a">return to the questions list</a> <a href="/q+a">return to the questions list</a>
</p> </p>

View file

@ -1,4 +1,7 @@
export * as layout from "../layout.tsx"; export * as layout from "../layout.tsx";
export const regenerate = {
manual: true,
};
export interface Input { export interface Input {
admin?: boolean; admin?: boolean;
@ -11,7 +14,7 @@ export const meta: Metadata = {
<const/{ admin = false } = input /> <const/{ admin = false } = input />
<const/questions = [...Question.getAll()] /> <const/questions = [...Question.getAll()] />
<if=!admin> <if=true>
<question-form /> <question-form />
</> </>
<for|question| of=questions> <for|question| of=questions>

View file

@ -1,9 +1,8 @@
import { basicSetup, EditorState, EditorView } from "codemirror"; import { EditorState } from "@codemirror/state";
import { basicSetup, EditorView } from "codemirror";
import { ssrSync } from "#ssr"; import { ssrSync } from "#ssr";
// @ts-ignore import { ScriptPayload } from "@/q+a/view/editor.marko";
import type { ScriptPayload } from "../views/editor.marko"; import QuestionRender from "@/q+a/tags/question.marko";
// @ts-ignore
import QuestionRender from "@/q+a/components/Question.marko";
declare const payload: ScriptPayload; declare const payload: ScriptPayload;
const date = new Date(payload.date); const date = new Date(payload.date);
@ -21,9 +20,11 @@ function updatePreview(text: string) {
type: payload.type, type: payload.type,
date, date,
}} }}
editor
/>, />,
).text; ).text;
} }
updatePreview(payload.text);
const startState = EditorState.create({ const startState = EditorState.create({
doc: payload.text, doc: payload.text,

View file

@ -2,12 +2,13 @@
export interface Input { export interface Input {
question: Question; question: Question;
admin?: boolean; admin?: boolean;
editor?: boolean;
} }
// 2024-12-31 05:00:00 EST // 2024-12-31 05:00:00 EST
export const transitionDate = 1735639200000; export const transitionDate = 1735639200000;
<const/{ question, admin } = input /> <const/{ question, admin = false, editor = false } = input />
<const/{ id, date } = question/> <const/{ id, date } = question/>
<${"e-"} <${"e-"}
@ -30,8 +31,11 @@ export 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.client.ts"; <if=!editor>
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";
import { CloverMarkdown } from "@/q+a/clover-markdown.tsx"; import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
import { addScript as Script } from "#sitegen"

View file

@ -56,10 +56,12 @@ export interface ScriptPayload {
</div> </div>
<div#editor /> <div#editor />
<main#preview> <main#preview>
<question ...{question} /> <question editor ...{ question } />
</main> </main>
</div> </div>
<html-script src="/js/edit_frontend.js" type="module" /> <html-script>self.payload=${devalue.uneval(payload)}</html-script>
<html-script src="/js/q+a/scripts/editor.js" type="module" />
import * as devalue from 'devalue';
import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts"; import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";
import { Question, QuestionType } from "@/q+a/models/Question.ts"; import { Question, QuestionType } from "@/q+a/models/Question.ts";

View file

@ -1,23 +1,25 @@
export * as layout from "@/q+a/layout.tsx";
export interface Input { export interface Input {
question: Question; question: Question;
} }
server export function meta({ context: { req }, question }) { server export function meta({ context, question }) {
const isDiscord = req.get("user-agent") const isDiscord = context.get("user-agent")
?.toLowerCase() ?.toLowerCase()
.includes("discordbot"); .includes("discordbot");
if (question.type === QuestionType.normal) { if (question.type === QuestionType.normal) {
return { return {
title: "question permalink", title: "question permalink",
openGraph: { openGraph: {
images: [{ url: `https://paperclover.net/q+a/${q.id}.png` }], images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
}, },
twitter: { card: "summary_large_image" }, twitter: { card: "summary_large_image" },
themeColor: isDiscord themeColor: isDiscord
? q.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71" ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
: undefined, : undefined,
}; };
} }
return { title: 'question permalink' };
} }
<const/{ question }=input/> <const/{ question }=input/>