| 1 | #!/usr/bin/env node |
| 2 | // This file allows using Node.js in combination with |
| 3 | // all available plugins. Usage: "node run <script>" |
| 4 | import * as child_process from "node:child_process"; |
| 5 | import * as crypto from "node:crypto"; |
| 6 | import * as fs from "node:fs"; |
| 7 | import * as os from "node:os"; |
| 8 | import * as path from "node:path"; |
| 9 | import process from "node:process"; |
| 10 | import * as url from "node:url"; |
| 11 | import * as zlib from "node:zlib"; |
| 12 | |
| 13 | // Disable experimental warnings (Type Stripping, etc) |
| 14 | const { emit: originalEmit } = process; |
| 15 | const warnings = ["ExperimentalWarning"]; |
| 16 | process.emit = function(event, error) { |
| 17 | return event === "warning" && warnings.includes(error.name) |
| 18 | ? false |
| 19 | : originalEmit.apply(process, arguments); |
| 20 | }; |
| 21 | |
| 22 | // Auto-install with npm if needed. |
| 23 | { |
| 24 | const rootDir = path.dirname(url.fileURLToPath(import.meta.url)); |
| 25 | const cloverDir = path.join(rootDir, ".clover"); |
| 26 | fs.mkdirSync(cloverDir, { recursive: true }); |
| 27 | function computeHash() { |
| 28 | const hash = crypto.createHash("sha1"); |
| 29 | hash.update("npm"); |
| 30 | hash.update(fs.readFileSync(path.join(rootDir, "package.json"))); |
| 31 | hash.update(fs.readFileSync(path.join(rootDir, "package-lock.json"))); |
| 32 | hash.update(fs.readdirSync(path.join(rootDir, "node_modules")).join("\0")); |
| 33 | return hash.digest("hex"); |
| 34 | } |
| 35 | try { |
| 36 | const expected = fs.readFileSync(path.join(cloverDir, "deps-hash.txt"), "utf-8"); |
| 37 | const actual = computeHash(); |
| 38 | if (expected !== actual) throw new Error(); |
| 39 | } catch { |
| 40 | child_process.execSync("npm i --no-audit --no-fund", { stdio: "inherit" }); |
| 41 | console.info(); |
| 42 | fs.writeFileSync(path.join(cloverDir, "deps-hash.txt"), computeHash()); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | let hot; |
| 47 | try { |
| 48 | // zstd is not supported in node 22 |
| 49 | await new Promise((resolve) => zlib.zstdCompress(new Uint8Array(), resolve)); |
| 50 | |
| 51 | // seems to fail in bun, run it earlier to catch |
| 52 | (await import("source-map-support")).install({ hookRequire: true }); |
| 53 | |
| 54 | // init hooks. some runtimes have bugs here. and long term this system should be rebuilt |
| 55 | hot = await import("./framework/hot.ts"); |
| 56 | hot.load("./lib/log.ts"); |
| 57 | } catch (e) { |
| 58 | const brand = process.versions.bun |
| 59 | ? `bun ${process.versions.bun}` |
| 60 | : process.versions.deno |
| 61 | ? `deno ${process.versions.deno}` |
| 62 | : null; |
| 63 | const brandText = brand ? ` (${brand})` : ""; |
| 64 | globalThis.console.error( |
| 65 | `sitegen depends on a node.js v24. your runtime is missing features.\n` |
| 66 | + `this is node.js version ${process.version}${brandText}\n\n` |
| 67 | + `get node.js --> https://nodejs.org/en/download/current\n`, |
| 68 | ); |
| 69 | globalThis.console.error(e); |
| 70 | process.exit(1); |
| 71 | await new Promise(() => {}); |
| 72 | } |
| 73 | |
| 74 | const log = hot.load("./lib/log.ts"); |
| 75 | console.info = log.info; |
| 76 | console.warn = log.warn; |
| 77 | console.error = log.error; |
| 78 | console.debug = log.debug; |
| 79 | console["log"] = log.log; |
| 80 | |
| 81 | process.on("uncaughtException", (error) => { |
| 82 | console.error("Uncaught Exception"); |
| 83 | console.error(error); |
| 84 | log.getDrawLock("long"); |
| 85 | process.exit(1); |
| 86 | }); |
| 87 | |
| 88 | if (fs.existsSync(".env")) { |
| 89 | const env = fs.readFileSync(".env", "utf-8") |
| 90 | .split("\n").filter(Boolean) |
| 91 | .map((x) => x.trim().split("=")); |
| 92 | for (const [k, v] of env) process.env[k] = v; |
| 93 | } |
| 94 | |
| 95 | // Load with hooks |
| 96 | if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) { |
| 97 | if (process.argv.length == 2) { |
| 98 | log.error("usage: node run <script> [...args]"); |
| 99 | process.exit(1); |
| 100 | } |
| 101 | let found; |
| 102 | for (const dir of fs.globSync("{lib,{src,framework},{src,lib}/**/bin}")) { |
| 103 | try { |
| 104 | found = hot.resolveFrom( |
| 105 | import.meta.filename, |
| 106 | `./${dir}/${process.argv[2]}`, |
| 107 | ); |
| 108 | break; |
| 109 | } catch (e) { |
| 110 | continue; |
| 111 | } |
| 112 | } |
| 113 | if (!found) { |
| 114 | log.error("Cannot find script: " + process.argv[2]); |
| 115 | process.exit(1); |
| 116 | } |
| 117 | process.argv = [process.argv[0], ...process.argv.slice(2)]; |
| 118 | try { |
| 119 | const mod = await hot.load(found); |
| 120 | if (mod.main) mod.main(); |
| 121 | else if (mod.default?.fetch) { |
| 122 | const protocol = "http"; |
| 123 | const { serve } = hot.load("@hono/node-server"); |
| 124 | serve( |
| 125 | { |
| 126 | fetch: mod.default.fetch, |
| 127 | port: mod.port ?? 3000, |
| 128 | }, |
| 129 | ({ address, port }) => { |
| 130 | if (address === "::" || address === "127.0.0.1") { |
| 131 | address = os.hostname().replace(/\.local$/, ""); |
| 132 | } |
| 133 | log.info( |
| 134 | url.format({ |
| 135 | protocol, |
| 136 | hostname: address, |
| 137 | port, |
| 138 | }), |
| 139 | ); |
| 140 | }, |
| 141 | ); |
| 142 | } else if (!found.match(/\.test\.[tj]sx?$/)) { |
| 143 | log.warn( |
| 144 | hot.load("path").relative(".", found), |
| 145 | "does not export a \"main\" function", |
| 146 | ); |
| 147 | } |
| 148 | } catch (e) { |
| 149 | log.error(e); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | export { hot }; |