authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-06-15 13:11:21-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:45-07:00
logfa86e0da8bb2677603cb5bcda64b565a362359a6
treede1e68df47cd8f90e57210af655dcb854bf7fcab
parentdad822b38eaec73a2696f012cd4e8a4117dd2e87
signature Commit is signed but in an unrecognized format.

finish q+a


20 files changed, 259 insertions(+), 68 deletions(-)

framework/bundle.ts+43-27
...@@ -23,7 +23,8 @@ export async function bundleClientJavaScript(...@@ -23,7 +23,8 @@ export async function bundleClientJavaScript(
23 }23 }
2424
25 const clientPlugins: esbuild.Plugin[] = [25 const clientPlugins: esbuild.Plugin[] = [
26 // There are currently no plugins needed by 'paperclover.net'26 projectRelativeResolution(),
27 markoViaBuildCache(incr),
27 ];28 ];
2829
29 const bundle = await esbuild.build({30 const bundle = await esbuild.build({
...@@ -42,6 +43,9 @@ export async function bundleClientJavaScript(...@@ -42,6 +43,9 @@ export async function bundleClientJavaScript(
42 jsx: "automatic",43 jsx: "automatic",
43 jsxImportSource: "#ssr",44 jsxImportSource: "#ssr",
44 jsxDev: dev,45 jsxDev: dev,
46 define: {
47 "ASSERT": "console.assert",
48 },
45 });49 });
46 if (bundle.errors.length || bundle.warnings.length) {50 if (bundle.errors.length || bundle.warnings.length) {
47 throw new AggregateError(51 throw new AggregateError(
...@@ -50,11 +54,15 @@ export async function bundleClientJavaScript(...@@ -50,11 +54,15 @@ export async function bundleClientJavaScript(
50 );54 );
51 }55 }
52 const publicScriptRoutes = extraPublicScripts.map((file) =>56 const publicScriptRoutes = extraPublicScripts.map((file) =>
53 path.basename(file).replace(/\.client\.[tj]sx?/, "")57 "/js/" +
58 path.relative(hot.projectSrc, file).replaceAll("\\", "/").replace(
59 /\.client\.[tj]sx?/,
60 ".js",
61 )
54 );62 );
55 const { metafile } = bundle;63 const { metafile, outputFiles } = bundle;
56 const promises: Promise<void>[] = [];64 const promises: Promise<void>[] = [];
57 for (const file of bundle.outputFiles) {65 for (const file of outputFiles) {
58 const { text } = file;66 const { text } = file;
59 let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");67 let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
60 const { inputs } = UNWRAP(metafile.outputs["out!" + route]);68 const { inputs } = UNWRAP(metafile.outputs["out!" + route]);
...@@ -63,8 +71,8 @@ export async function bundleClientJavaScript(...@@ -63,8 +71,8 @@ export async function bundleClientJavaScript(
63 // Register non-chunks as script entries.71 // Register non-chunks as script entries.
64 const chunk = route.startsWith("/js/c.");72 const chunk = route.startsWith("/js/c.");
65 if (!chunk) {73 if (!chunk) {
66 const key = hot.getScriptId(path.resolve(sources[0]));74 const key = hot.getScriptId(path.resolve(sources[sources.length - 1]));
67 route = "/js/" + key + ".js";75 route = "/js/" + key.replace(/\.client\.tsx?/, ".js");
68 incr.put({76 incr.put({
69 sources,77 sources,
70 kind: "script",78 kind: "script",
...@@ -123,27 +131,7 @@ export async function bundleServerJavaScript(...@@ -123,27 +131,7 @@ export async function bundleServerJavaScript(
123 "$views": viewSource,131 "$views": viewSource,
124 }),132 }),
125 projectRelativeResolution(),133 projectRelativeResolution(),
126 {134 markoViaBuildCache(incr),
127 name: "marko via build cache",
128 setup(b) {
129 b.onLoad(
130 { filter: /\.marko$/ },
131 async ({ path: file }) => {
132 const key = path.relative(hot.projectRoot, file)
133 .replaceAll("\\", "/");
134 const cacheEntry = incr.out.serverMarko.get(key);
135 if (!cacheEntry) {
136 throw new Error("Marko file not in cache: " + file);
137 }
138 return ({
139 loader: "ts",
140 contents: cacheEntry.src,
141 resolveDir: path.dirname(file),
142 });
143 },
144 );
145 },
146 },
147 {135 {
148 name: "replace client references",136 name: "replace client references",
149 setup(b) {137 setup(b) {
...@@ -168,6 +156,9 @@ export async function bundleServerJavaScript(...@@ -168,6 +156,9 @@ export async function bundleServerJavaScript(
168 },156 },
169 },157 },
170 ];158 ];
159 const pkg = await fs.readJson("package.json") as {
160 dependencies: Record<string, string>;
161 };
171 const { metafile, outputFiles } = await esbuild.build({162 const { metafile, outputFiles } = await esbuild.build({
172 bundle: true,163 bundle: true,
173 chunkNames: "c.[hash]",164 chunkNames: "c.[hash]",
...@@ -186,6 +177,8 @@ export async function bundleServerJavaScript(...@@ -186,6 +177,8 @@ export async function bundleServerJavaScript(
186 jsx: "automatic",177 jsx: "automatic",
187 jsxImportSource: "#ssr",178 jsxImportSource: "#ssr",
188 jsxDev: false,179 jsxDev: false,
180 external: Object.keys(pkg.dependencies)
181 .filter((x) => !x.startsWith("@paperclover")),
189 });182 });
190183
191 const files: Record<string, Buffer> = {};184 const files: Record<string, Buffer> = {};
...@@ -279,6 +272,29 @@ export async function finalizeServerJavaScript(...@@ -279,6 +272,29 @@ export async function finalizeServerJavaScript(
279 });272 });
280}273}
281274
275function markoViaBuildCache(incr: Incremental): esbuild.Plugin {
276 return {
277 name: "marko via build cache",
278 setup(b) {
279 b.onLoad(
280 { filter: /\.marko$/ },
281 async ({ path: file }) => {
282 const key = path.relative(hot.projectRoot, file)
283 .replaceAll("\\", "/");
284 const cacheEntry = incr.out.serverMarko.get(key);
285 if (!cacheEntry) {
286 throw new Error("Marko file not in cache: " + file);
287 }
288 return ({
289 loader: "ts",
290 contents: cacheEntry.src,
291 resolveDir: path.dirname(file),
292 });
293 },
294 );
295 },
296 };
297}
282import * as esbuild from "esbuild";298import * as esbuild from "esbuild";
283import * as path from "node:path";299import * as path from "node:path";
284import process from "node:process";300import process from "node:process";
framework/engine/marko-runtime.ts+1-3
...@@ -52,7 +52,6 @@ export const dynamicTag = (...@@ -52,7 +52,6 @@ export const dynamicTag = (
52 if (typeof tag === "function") { 52 if (typeof tag === "function") {
53 clover: { 53 clover: {
54 const unwrapped = (tag as any).unwrapped; 54 const unwrapped = (tag as any).unwrapped;
55 console.log({ tag, unwrapped });
56 if (unwrapped) { 55 if (unwrapped) {
57 tag = unwrapped; 56 tag = unwrapped;
58 break clover; 57 break clover;
...@@ -130,7 +129,7 @@ export function escapeXML(input: unknown) {...@@ -130,7 +129,7 @@ export function escapeXML(input: unknown) {
130 ) { 129 ) {
131 throw new Error( 130 throw new Error(
132 `Unexpected object in template placeholder: '` + 131 `Unexpected object in template placeholder: '` +
133 util.inspect({ name: "clover" }) + "'. " + 132 engine.inspect({ name: "clover" }) + "'. " +
134 `To emit a literal '[object Object]', use \${String(value)}`, 133 `To emit a literal '[object Object]', use \${String(value)}`,
135 ); 134 );
136 } 135 }
...@@ -145,4 +144,3 @@ import * as engine from "./ssr.ts";...@@ -145,4 +144,3 @@ import * as engine from "./ssr.ts";
145import type { ServerRenderer } from "marko/html/template"; 144import type { ServerRenderer } from "marko/html/template";
146import { type Accessor } from "marko/common/types"; 145import { type Accessor } from "marko/common/types";
147import * as marko from "#marko/html"; 146import * as marko from "#marko/html";
148import * as util from "node:util";
framework/generate.ts+9-6
...@@ -8,9 +8,9 @@ export function main() {...@@ -8,9 +8,9 @@ export function main() {
8 successText,8 successText,
9 failureText: () => "sitegen FAIL",9 failureText: () => "sitegen FAIL",
10 }, async (spinner) => {10 }, async (spinner) => {
11 const incr = Incremental.fromDisk();11 // const incr = Incremental.fromDisk();
12 await incr.statAllFiles();12 // await incr.statAllFiles();
13 // const incr = new Incremental();13 const incr = new Incremental();
14 const result = await sitegen(spinner, incr);14 const result = await sitegen(spinner, incr);
15 incr.toDisk(); // Allows picking up this state again15 incr.toDisk(); // Allows picking up this state again
16 return result;16 return result;
...@@ -122,7 +122,6 @@ export async function sitegen(...@@ -122,7 +122,6 @@ export async function sitegen(
122 }122 }
123 }123 }
124 }124 }
125 scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
126 const globalCssPath = join("global.css");125 const globalCssPath = join("global.css");
127126
128 // TODO: make sure that `static` and `pages` does not overlap127 // TODO: make sure that `static` and `pages` does not overlap
...@@ -311,8 +310,12 @@ export async function sitegen(...@@ -311,8 +310,12 @@ export async function sitegen(
311 maxJobs: 2,310 maxJobs: 2,
312 });311 });
313 viewQueue.addMany(neededViews);312 viewQueue.addMany(neededViews);
314 await pageQueue.done({ method: "stop" });313 const pageAndViews = [
315 await viewQueue.done({ method: "stop" });314 pageQueue.done({ method: "stop" }),
315 viewQueue.done({ method: "stop" }),
316 ];
317 await Promise.allSettled(pageAndViews);
318 await Promise.all(pageAndViews);
316 status.format = spinnerFormat;319 status.format = spinnerFormat;
317320
318 // -- bundle server javascript (backend and views) --321 // -- bundle server javascript (backend and views) --
framework/hot.ts+5-1
...@@ -63,7 +63,7 @@ Module.prototype._compile = function (...@@ -63,7 +63,7 @@ Module.prototype._compile = function (
63 if (shouldTrackPath(filename)) {63 if (shouldTrackPath(filename)) {
64 const cssImportsMaybe: string[] = [];64 const cssImportsMaybe: string[] = [];
65 const imports: string[] = [];65 const imports: string[] = [];
66 for (const { filename: file } of this.children) {66 for (const { filename: file, cloverClientRefs } of this.children) {
67 if (file.endsWith(".css")) cssImportsMaybe.push(file);67 if (file.endsWith(".css")) cssImportsMaybe.push(file);
68 else {68 else {
69 const child = fileStats.get(file);69 const child = fileStats.get(file);
...@@ -71,6 +71,10 @@ Module.prototype._compile = function (...@@ -71,6 +71,10 @@ Module.prototype._compile = function (
71 const { cssImportsRecursive } = child;71 const { cssImportsRecursive } = child;
72 if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive);72 if (cssImportsRecursive) cssImportsMaybe.push(...cssImportsRecursive);
73 imports.push(file);73 imports.push(file);
74 if (cloverClientRefs && cloverClientRefs.length > 0) {
75 (this.cloverClientRefs ??= [])
76 .push(...cloverClientRefs);
77 }
74 }78 }
75 }79 }
76 fileStats.set(filename, {80 fileStats.set(filename, {
framework/incremental.ts+1-1
...@@ -148,7 +148,7 @@ export class Incremental {...@@ -148,7 +148,7 @@ export class Incremental {
148 for (const key of map.keys()) {148 for (const key of map.keys()) {
149 if (!this.round.referenced.has(`${kind}\0${key}`)) {149 if (!this.round.referenced.has(`${kind}\0${key}`)) {
150 unreferenced.push({ kind: kind as ArtifactKind, key });150 unreferenced.push({ kind: kind as ArtifactKind, key });
151 this.out[kind as ArtifactKind].delete(key);151 // this.out[kind as ArtifactKind].delete(key);
152 }152 }
153 }153 }
154 }154 }
framework/lib/assets.ts-2
...@@ -11,7 +11,6 @@ export async function reload() {...@@ -11,7 +11,6 @@ export async function reload() {
11 fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),11 fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),
12 fs.readFile(path.join(import.meta.dirname, "static.blob")),12 fs.readFile(path.join(import.meta.dirname, "static.blob")),
13 ]);13 ]);
14 console.log("new buffer loaded");
15 assets = {14 assets = {
16 map: JSON.parse(map),15 map: JSON.parse(map),
17 buf,16 buf,
...@@ -110,7 +109,6 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {...@@ -110,7 +109,6 @@ function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
110}109}
111110
112process.on("message", (msg: any) => {111process.on("message", (msg: any) => {
113 console.log({ msg });
114 if (msg?.type === "clover.assets.reload") reload();112 if (msg?.type === "clover.assets.reload") reload();
115});113});
116114
framework/watch.ts+2-2
...@@ -58,8 +58,8 @@ export async function main() {...@@ -58,8 +58,8 @@ export async function main() {
58 successText: generate.successText, 58 successText: generate.successText,
59 failureText: () => "sitegen FAIL", 59 failureText: () => "sitegen FAIL",
60 }, async (spinner) => { 60 }, async (spinner) => {
61 console.log("---"); 61 console.info("---");
62 console.log( 62 console.info(
63 "Updated" + 63 "Updated" +
64 (changed.length === 1 64 (changed.length === 1
65 ? " " + changed[0] 65 ? " " + changed[0]
package-lock.json+152
...@@ -8,6 +8,8 @@...@@ -8,6 +8,8 @@
8 "@hono/node-server": "^1.14.3",8 "@hono/node-server": "^1.14.3",
9 "@mdx-js/mdx": "^3.1.0",9 "@mdx-js/mdx": "^3.1.0",
10 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",10 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
11 "codemirror": "^6.0.1",
12 "devalue": "^5.1.1",
11 "esbuild": "^0.25.5",13 "esbuild": "^0.25.5",
12 "hls.js": "^1.6.5",14 "hls.js": "^1.6.5",
13 "hono": "^4.7.11",15 "hono": "^4.7.11",
...@@ -404,6 +406,87 @@...@@ -404,6 +406,87 @@
404 "node": ">=6.9.0"406 "node": ">=6.9.0"
405 }407 }
406 },408 },
409 "node_modules/@codemirror/autocomplete": {
410 "version": "6.18.6",
411 "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
412 "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
413 "license": "MIT",
414 "dependencies": {
415 "@codemirror/language": "^6.0.0",
416 "@codemirror/state": "^6.0.0",
417 "@codemirror/view": "^6.17.0",
418 "@lezer/common": "^1.0.0"
419 }
420 },
421 "node_modules/@codemirror/commands": {
422 "version": "6.8.1",
423 "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
424 "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
425 "license": "MIT",
426 "dependencies": {
427 "@codemirror/language": "^6.0.0",
428 "@codemirror/state": "^6.4.0",
429 "@codemirror/view": "^6.27.0",
430 "@lezer/common": "^1.1.0"
431 }
432 },
433 "node_modules/@codemirror/language": {
434 "version": "6.11.1",
435 "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
436 "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
437 "license": "MIT",
438 "dependencies": {
439 "@codemirror/state": "^6.0.0",
440 "@codemirror/view": "^6.23.0",
441 "@lezer/common": "^1.1.0",
442 "@lezer/highlight": "^1.0.0",
443 "@lezer/lr": "^1.0.0",
444 "style-mod": "^4.0.0"
445 }
446 },
447 "node_modules/@codemirror/lint": {
448 "version": "6.8.5",
449 "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
450 "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
451 "license": "MIT",
452 "dependencies": {
453 "@codemirror/state": "^6.0.0",
454 "@codemirror/view": "^6.35.0",
455 "crelt": "^1.0.5"
456 }
457 },
458 "node_modules/@codemirror/search": {
459 "version": "6.5.11",
460 "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
461 "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
462 "license": "MIT",
463 "dependencies": {
464 "@codemirror/state": "^6.0.0",
465 "@codemirror/view": "^6.0.0",
466 "crelt": "^1.0.5"
467 }
468 },
469 "node_modules/@codemirror/state": {
470 "version": "6.5.2",
471 "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
472 "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
473 "license": "MIT",
474 "dependencies": {
475 "@marijn/find-cluster-break": "^1.0.0"
476 }
477 },
478 "node_modules/@codemirror/view": {
479 "version": "6.37.2",
480 "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
481 "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
482 "license": "MIT",
483 "dependencies": {
484 "@codemirror/state": "^6.5.0",
485 "crelt": "^1.0.6",
486 "style-mod": "^4.1.0",
487 "w3c-keyname": "^2.2.4"
488 }
489 },
407 "node_modules/@esbuild/aix-ppc64": {490 "node_modules/@esbuild/aix-ppc64": {
408 "version": "0.25.5",491 "version": "0.25.5",
409 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",492 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
...@@ -864,6 +947,30 @@...@@ -864,6 +947,30 @@
864 "@jridgewell/sourcemap-codec": "^1.4.14"947 "@jridgewell/sourcemap-codec": "^1.4.14"
865 }948 }
866 },949 },
950 "node_modules/@lezer/common": {
951 "version": "1.2.3",
952 "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
953 "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
954 "license": "MIT"
955 },
956 "node_modules/@lezer/highlight": {
957 "version": "1.2.1",
958 "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
959 "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
960 "license": "MIT",
961 "dependencies": {
962 "@lezer/common": "^1.0.0"
963 }
964 },
965 "node_modules/@lezer/lr": {
966 "version": "1.4.2",
967 "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
968 "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
969 "license": "MIT",
970 "dependencies": {
971 "@lezer/common": "^1.0.0"
972 }
973 },
867 "node_modules/@luxass/strip-json-comments": {974 "node_modules/@luxass/strip-json-comments": {
868 "version": "1.4.0",975 "version": "1.4.0",
869 "resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz",976 "resolved": "https://registry.npmjs.org/@luxass/strip-json-comments/-/strip-json-comments-1.4.0.tgz",
...@@ -873,6 +980,12 @@...@@ -873,6 +980,12 @@
873 "node": ">=18"980 "node": ">=18"
874 }981 }
875 },982 },
983 "node_modules/@marijn/find-cluster-break": {
984 "version": "1.0.2",
985 "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
986 "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
987 "license": "MIT"
988 },
876 "node_modules/@marko/compiler": {989 "node_modules/@marko/compiler": {
877 "version": "5.39.21",990 "version": "5.39.21",
878 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz",991 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.21.tgz",
...@@ -1458,6 +1571,21 @@...@@ -1458,6 +1571,21 @@
1458 "node": ">=8"1571 "node": ">=8"
1459 }1572 }
1460 },1573 },
1574 "node_modules/codemirror": {
1575 "version": "6.0.1",
1576 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
1577 "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
1578 "license": "MIT",
1579 "dependencies": {
1580 "@codemirror/autocomplete": "^6.0.0",
1581 "@codemirror/commands": "^6.0.0",
1582 "@codemirror/language": "^6.0.0",
1583 "@codemirror/lint": "^6.0.0",
1584 "@codemirror/search": "^6.0.0",
1585 "@codemirror/state": "^6.0.0",
1586 "@codemirror/view": "^6.0.0"
1587 }
1588 },
1461 "node_modules/collapse-white-space": {1589 "node_modules/collapse-white-space": {
1462 "version": "2.1.0",1590 "version": "2.1.0",
1463 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",1591 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
...@@ -1537,6 +1665,12 @@...@@ -1537,6 +1665,12 @@
1537 }1665 }
1538 }1666 }
1539 },1667 },
1668 "node_modules/crelt": {
1669 "version": "1.0.6",
1670 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
1671 "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
1672 "license": "MIT"
1673 },
1540 "node_modules/csstype": {1674 "node_modules/csstype": {
1541 "version": "3.1.3",1675 "version": "3.1.3",
1542 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",1676 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
...@@ -1605,6 +1739,12 @@...@@ -1605,6 +1739,12 @@
1605 "node": ">=6"1739 "node": ">=6"
1606 }1740 }
1607 },1741 },
1742 "node_modules/devalue": {
1743 "version": "5.1.1",
1744 "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
1745 "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
1746 "license": "MIT"
1747 },
1608 "node_modules/devlop": {1748 "node_modules/devlop": {
1609 "version": "1.1.0",1749 "version": "1.1.0",
1610 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",1750 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
...@@ -3748,6 +3888,12 @@...@@ -3748,6 +3888,12 @@
3748 "url": "https://github.com/chalk/strip-ansi?sponsor=1"3888 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
3749 }3889 }
3750 },3890 },
3891 "node_modules/style-mod": {
3892 "version": "4.1.2",
3893 "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
3894 "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
3895 "license": "MIT"
3896 },
3751 "node_modules/style-to-js": {3897 "node_modules/style-to-js": {
3752 "version": "1.1.16",3898 "version": "1.1.16",
3753 "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",3899 "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
...@@ -4020,6 +4166,12 @@...@@ -4020,6 +4166,12 @@
4020 "url": "https://opencollective.com/unified"4166 "url": "https://opencollective.com/unified"
4021 }4167 }
4022 },4168 },
4169 "node_modules/w3c-keyname": {
4170 "version": "2.2.8",
4171 "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
4172 "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
4173 "license": "MIT"
4174 },
4023 "node_modules/wrap-ansi": {4175 "node_modules/wrap-ansi": {
4024 "version": "7.0.0",4176 "version": "7.0.0",
4025 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",4177 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
package.json+2
...@@ -4,6 +4,8 @@...@@ -4,6 +4,8 @@
4 "@hono/node-server": "^1.14.3",4 "@hono/node-server": "^1.14.3",
5 "@mdx-js/mdx": "^3.1.0",5 "@mdx-js/mdx": "^3.1.0",
6 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",6 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
7 "codemirror": "^6.0.1",
8 "devalue": "^5.1.1",
7 "esbuild": "^0.25.5",9 "esbuild": "^0.25.5",
8 "hls.js": "^1.6.5",10 "hls.js": "^1.6.5",
9 "hono": "^4.7.11",11 "hono": "^4.7.11",
src/q+a/backend.ts+4-9
...@@ -127,9 +127,9 @@ app.get("/q+a/:id", async (c, next) => {...@@ -127,9 +127,9 @@ app.get("/q+a/:id", async (c, next) => {
127 const question = Question.getByDate(timestamp); 127 const question = Question.getByDate(timestamp);
128 if (!question) return next(); 128 if (!question) return next();
129 129
130 // if (image) { 130 if (image) {
131 // return getQuestionImage(question, c.req.method === "HEAD"); 131 return getQuestionImage(question, c.req.method === "HEAD");
132 // } 132 }
133 return renderView(c, "q+a/permalink", { question }); 133 return renderView(c, "q+a/permalink", { question });
134}); 134});
135 135
...@@ -193,11 +193,6 @@ app.get("/q+a/things/random", async (c) => {...@@ -193,11 +193,6 @@ app.get("/q+a/things/random", async (c) => {
193 c.res = await renderView(c, "q+a/things-random", {}); 193 c.res = await renderView(c, "q+a/things-random", {});
194}); 194});
195 195
196// 404
197app.get("/q+a/*", async (c) => {
198 return serveAsset(c, "/q+a/404", 404);
199});
200
201async function questionFailure( 196async function questionFailure(
202 c: Context, 197 c: Context,
203 status: ContentfulStatusCode, 198 status: ContentfulStatusCode,
...@@ -229,5 +224,5 @@ import {...@@ -229,5 +224,5 @@ import {
229} from "./models/PendingQuestion.ts"; 224} from "./models/PendingQuestion.ts";
230import { Question, QuestionType } from "./models/Question.ts"; 225import { Question, QuestionType } from "./models/Question.ts";
231import { renderView } from "#sitegen/view"; 226import { renderView } from "#sitegen/view";
232// import { getQuestionImage } from "./question_image"; 227import { getQuestionImage } from "./image.tsx";
233import { formatQuestionId, questionIdToTimestamp } from "./format.ts"; 228import { formatQuestionId, questionIdToTimestamp } from "./format.ts";
src/q+a/clover-markdown.tsx+1-1
...@@ -211,7 +211,7 @@ function renderNode(node: string | ASTNode | ASTNode[]): any {...@@ -211,7 +211,7 @@ function renderNode(node: string | ASTNode | ASTNode[]): any {
211 if (typeof node === "string") {211 if (typeof node === "string") {
212 return node;212 return node;
213 } else if (Array.isArray(node)) {213 } else if (Array.isArray(node)) {
214 return node.flatMap((item) => renderNode(item));214 return node.map((item) => renderNode(item));
215 } else if (node.type === "text") {215 } else if (node.type === "text") {
216 return node.content;216 return node.content;
217 } else {217 } else {
src/q+a/header.client.ts+8
...@@ -1,4 +1,12 @@...@@ -1,4 +1,12 @@
1const h1 = document.querySelector("h1")!;1const h1 = document.querySelector("h1")!;
2const key = "net.paperclover.q+a.header";
3const state = localStorage?.getItem(key);
4if (state === "detrevni ton") h1.classList.toggle("invert");
2h1.addEventListener("click", () => {5h1.addEventListener("click", () => {
3 h1.classList.toggle("invert");6 h1.classList.toggle("invert");
7 localStorage?.setItem(
8 key,
9 (localStorage.getItem?.(key) ?? "not inverted")
10 .split("").reverse().join(""),
11 );
4});12});
src/q+a/image.tsx+1-2
...@@ -5,14 +5,13 @@ const cacheImageDir = path.resolve(".clover/question_images");...@@ -5,14 +5,13 @@ const cacheImageDir = path.resolve(".clover/question_images");
5const getBrowser = RefCountedExpirable( 5const getBrowser = RefCountedExpirable(
6 () => 6 () =>
7 puppeteer.launch({ 7 puppeteer.launch({
8 // headless: false,
9 args: ["--no-sandbox", "--disable-setuid-sandbox"], 8 args: ["--no-sandbox", "--disable-setuid-sandbox"],
10 }), 9 }),
11 (b) => b.close(), 10 (b) => b.close(),
12); 11);
13 12
14export async function renderQuestionImage(question: Question) { 13export async function renderQuestionImage(question: Question) {
15 const html = await renderViewToString("q+a/embed-image", { question }); 14 const html = await renderViewToString("q+a/image-embed", { question });
16 15
17 // this browser session will be reused if multiple images are generated 16 // this browser session will be reused if multiple images are generated
18 // either at the same time or within a 5-minute time span. the dispose 17 // either at the same time or within a 5-minute time span. the dispose
src/q+a/pages/admin.q+a.marko+3
...@@ -6,6 +6,9 @@ export const meta: Metadata = {...@@ -6,6 +6,9 @@ export const meta: Metadata = {
6 title: "paper clover q+a",6 title: "paper clover q+a",
7 description: "ask clover a question",7 description: "ask clover a question",
8};8};
9export const regenerate = {
10 manual: true,
11};
912
10<const/inboxSize = PendingQuestion.getAll().length />13<const/inboxSize = PendingQuestion.getAll().length />
11<if=(inboxSize > 0)>14<if=(inboxSize > 0)>
src/q+a/pages/q+a.404.marko+1
...@@ -4,6 +4,7 @@ export const meta: Metadata = {...@@ -4,6 +4,7 @@ export const meta: Metadata = {
4};4};
55
6<h2>sound the alarms</h2>6<h2>sound the alarms</h2>
7<p>this page doesn't exist</p>
7<p>8<p>
8 <a href="/q+a">return to the questions list</a>9 <a href="/q+a">return to the questions list</a>
9</p>10</p>
src/q+a/pages/q+a.marko+4-1
...@@ -1,4 +1,7 @@...@@ -1,4 +1,7 @@
1export * as layout from "../layout.tsx";1export * as layout from "../layout.tsx";
2export const regenerate = {
3 manual: true,
4};
25
3export interface Input {6export interface Input {
4 admin?: boolean;7 admin?: boolean;
...@@ -11,7 +14,7 @@ export const meta: Metadata = {...@@ -11,7 +14,7 @@ export const meta: Metadata = {
11<const/{ admin = false } = input />14<const/{ admin = false } = input />
12<const/questions = [...Question.getAll()] />15<const/questions = [...Question.getAll()] />
1316
14<if=!admin>17<if=true>
15 <question-form />18 <question-form />
16</>19</>
17<for|question| of=questions>20<for|question| of=questions>
src/q+a/scripts/editor.client.tsx+6-5
...@@ -1,9 +1,8 @@...@@ -1,9 +1,8 @@
1import { basicSetup, EditorState, EditorView } from "codemirror"; 1import { EditorState } from "@codemirror/state";
2import { basicSetup, EditorView } from "codemirror";
2import { ssrSync } from "#ssr"; 3import { ssrSync } from "#ssr";
3// @ts-ignore 4import { ScriptPayload } from "@/q+a/view/editor.marko";
4import type { ScriptPayload } from "../views/editor.marko"; 5import QuestionRender from "@/q+a/tags/question.marko";
5// @ts-ignore
6import QuestionRender from "@/q+a/components/Question.marko";
7 6
8declare const payload: ScriptPayload; 7declare const payload: ScriptPayload;
9const date = new Date(payload.date); 8const date = new Date(payload.date);
...@@ -21,9 +20,11 @@ function updatePreview(text: string) {...@@ -21,9 +20,11 @@ function updatePreview(text: string) {
21 type: payload.type, 20 type: payload.type,
22 date, 21 date,
23 }} 22 }}
23 editor
24 />, 24 />,
25 ).text; 25 ).text;
26} 26}
27updatePreview(payload.text);
27 28
28const startState = EditorState.create({ 29const startState = EditorState.create({
29 doc: payload.text, 30 doc: payload.text,
src/q+a/tags/question.marko+6-2
...@@ -2,12 +2,13 @@...@@ -2,12 +2,13 @@
2export interface Input {2export interface Input {
3 question: Question;3 question: Question;
4 admin?: boolean;4 admin?: boolean;
5 editor?: boolean;
5}6}
67
7// 2024-12-31 05:00:00 EST8// 2024-12-31 05:00:00 EST
8export const transitionDate = 1735639200000; 9export const transitionDate = 1735639200000;
910
10<const/{ question, admin } = input />11<const/{ question, admin = false, editor = false } = input />
11<const/{ id, date } = question/>12<const/{ id, date } = question/>
1213
13<${"e-"}14<${"e-"}
...@@ -30,8 +31,11 @@ export const transitionDate = 1735639200000;...@@ -30,8 +31,11 @@ export const transitionDate = 1735639200000;
30</>31</>
3132
32// this singleton script will make all the '<time>' tags clickable.33// this singleton script will make all the '<time>' tags clickable.
33client import "./clickable-links.client.ts";34<if=!editor>
35 client import "./clickable-links.client.ts";
36</>
3437
35import type { Question } from "@/q+a/models/Question.ts";38import type { Question } from "@/q+a/models/Question.ts";
36import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";39import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
37import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";40import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
41import { addScript as Script } from "#sitegen"
src/q+a/views/editor.marko+4-2
...@@ -56,10 +56,12 @@ export interface ScriptPayload {...@@ -56,10 +56,12 @@ export interface ScriptPayload {
56 </div>56 </div>
57 <div#editor />57 <div#editor />
58 <main#preview>58 <main#preview>
59 <question ...{question} />59 <question editor ...{ question } />
60 </main>60 </main>
61</div>61</div>
62<html-script src="/js/edit_frontend.js" type="module" />62<html-script>self.payload=${devalue.uneval(payload)}</html-script>
63<html-script src="/js/q+a/scripts/editor.js" type="module" />
6364
65import * as devalue from 'devalue';
64import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";66import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";
65import { Question, QuestionType } from "@/q+a/models/Question.ts";67import { Question, QuestionType } from "@/q+a/models/Question.ts";
src/q+a/views/permalink.marko+6-4
...@@ -1,23 +1,25 @@...@@ -1,23 +1,25 @@
1export * as layout from "@/q+a/layout.tsx";
1export interface Input { 2export interface Input {
2 question: Question; 3 question: Question;
3} 4}
4 5
5server export function meta({ context: { req }, question }) { 6server export function meta({ context, question }) {
6 const isDiscord = req.get("user-agent") 7 const isDiscord = context.get("user-agent")
7 ?.toLowerCase() 8 ?.toLowerCase()
8 .includes("discordbot"); 9 .includes("discordbot");
9 if (question.type === QuestionType.normal) { 10 if (question.type === QuestionType.normal) {
10 return { 11 return {
11 title: "question permalink", 12 title: "question permalink",
12 openGraph: { 13 openGraph: {
13 images: [{ url: `https://paperclover.net/q+a/${q.id}.png` }], 14 images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
14 }, 15 },
15 twitter: { card: "summary_large_image" }, 16 twitter: { card: "summary_large_image" },
16 themeColor: isDiscord 17 themeColor: isDiscord
17 ? q.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71" 18 ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
18 : undefined, 19 : undefined,
19 }; 20 };
20 } 21 }
22 return { title: 'question permalink' };
21} 23}
22 24
23<const/{ question }=input/> 25<const/{ question }=input/>