1#!/usr/bin/env node
2// This file allows using Node.js in combination with all available plugins.
3// Usage: "node run [script] [...args]". Defaults to running paperclover.net.
4import child_process from "node:child_process";
5import crypto from "node:crypto";
6import fs from "node:fs";
7import path from "node:path";
8import process from "node:process";
9import url from "node:url";
10import * as incremental from "./lib/incremental.ts";
11
12const root = path.dirname(url.fileURLToPath(import.meta.url));
13
14// `supervise` allows restarting the entire app when something like `incremental.ts` is edited.
15await incremental.supervise(async () => {
16 // auto-install packages with npm if needed.
17 {
18 const cloverDir = path.join(root, ".clover");
19 fs.mkdirSync(cloverDir, { recursive: true });
20 function computeHash() {
21 const hash = crypto.createHash("sha1");
22 hash.update("npm");
23 hash.update(fs.readFileSync(path.join(root, "package.json")));
24 hash.update(fs.readFileSync(path.join(root, "package-lock.json")));
25 hash.update(fs.readdirSync(path.join(root, "node_modules")).join("\0"));
26 return hash.digest("hex");
27 }
28 try {
29 const expected = fs.readFileSync(path.join(cloverDir, "deps-hash.txt"), "utf-8");
30 const actual = computeHash();
31 if (expected !== actual) throw new Error();
32 } catch {
33 child_process.execSync("npm i --no-audit --no-fund", { stdio: "inherit" });
34 console.info();
35 fs.writeFileSync(path.join(cloverDir, "deps-hash.txt"), computeHash());
36 }
37 }
38
39 // variables that are already set win over the ones in `.env`
40 if (fs.existsSync(".env")) process.loadEnvFile(".env");
41
42 // everything but `incremental` itself loads inside of the graph
43 const markoLoader = incremental.lazyLoader(import.meta.resolve("./framework/marko.ts"));
44 const graph = new incremental.Graph({
45 db: ".clover/incremental.db",
46 loaders: {
47 ".marko": markoLoader,
48 ".mdo": markoLoader,
49 // stylesheets are found among a page's imports, and bundled apart from its code
50 ".css": () => ({ format: "serializable", content: null }),
51 ".txt": ({ source }) => ({ format: "serializable", content: source }),
52 },
53 });
54 const log = await graph.import(import.meta.resolve("@clo/lib/log"));
55 const ansi = await graph.import(import.meta.resolve("@clo/lib/string/ansi"));
56 const stack = await graph.import(import.meta.resolve("@clo/lib/log/stack"));
57
58 process.on("uncaughtException", (error) => {
59 log.writeOutput(
60 ansi.style(ansi.fgBlack + ansi.bgRed, " UNCAUGHT ") + " " + stack.format(error, process.stderr.isTTY) + "\n",
61 );
62 log.getDrawLock("long");
63 process.exit(1);
64 });
65
66 console.info = log.info;
67 console.warn = log.warn;
68 console.error = log.error;
69 console.debug = log.debug;
70 console.log = log.log;
71
72 // resolve entrypoint. a first argument that is a flag is for the site
73 const named = process.argv[2] !== undefined && !process.argv[2].startsWith("-");
74 const name = named ? process.argv[2] : "paperclover.net";
75 const mainFile = findScript(name);
76 if (!mainFile) {
77 log.error("Cannot find script: " + name);
78 process.exit(1);
79 }
80 // as if node.js ran the script itself
81 process.argv = [process.argv[0], mainFile, ...process.argv.slice(named ? 3 : 2)];
82
83 const module = await graph.import(mainFile, { main: true });
84 if (typeof module.main === "function") await module.main(graph);
85});
86
87/** a script by name, from the root or a `bin` folder, with or without an extension */
88function findScript(name) {
89 const dirs = [".", ...fs.globSync("{lib,src,framework,{src,lib}/**/bin}", { cwd: root })];
90 for (const dir of dirs) {
91 for (const ext of ["", ".ts", ".tsx", ".js", ".mjs"]) {
92 const file = path.join(root, dir, name + ext);
93 if (fs.statSync(file, { throwIfNoEntry: false })?.isFile()) return file;
94 }
95 }
96 return null;
97}