commit 0ef48e85f02b80c8a0d57825de17f3703f57451d Author: sophie Date: Fri Aug 2 04:25:05 2024 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9e30fc --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# (S)ophie's (S)erver (S)ite (G)enerator + +## Usage +`bun add git+https://git.sad.ovh/sophie/sssg` + + +## design goals: +1. rewrite and generate custom HTML/TS/JS/whatever.. +2. allow for variables and other important features in buildsystem->html +3. plugins, this ties together the top two +4. HMR and HTML/CSS reloading (in the Dev plugin) +5. Rewriteable file renaming +6. Every plugin runs on the same set of files, and can rename and reinject multiple times diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5a20e55 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..9833157 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "sssg", + "module": "index.ts", + "type": "module", + "main": "src/index.ts", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@types/mime-types": "^2.1.4", + "esbuild": "^0.23.0", + "marked": "^13.0.3", + "mime-types": "^2.1.35", + "preact": "^10.23.1" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7bed27e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,106 @@ +export abstract class Plugin { + abstract name: string; + abstract rewriteTriggers: string[]; + renameTo?: string; + abstract longLasting: boolean; + abstract build?(): void; + + abstract rewriteFile( + file: string, + filePath: string + ): Promise; +} + +import * as fs from "fs"; +import * as path from "path"; + +export default class SSSG { + plugins: Plugin[] = []; + outputFolder!: string; + inputFolder!: string; + constructor({ + outputFolder, + inputFolder, + }: { + outputFolder: string; + inputFolder: string; + }) { + this.inputFolder = inputFolder; + this.outputFolder = outputFolder; + } + + async run({ plugins }: { plugins: Plugin[] }) { + this.plugins = plugins; + if (!fs.existsSync(this.outputFolder)) fs.mkdirSync(this.outputFolder); + } + + async build() { + console.log("build triggered") + for(const plugin of this.plugins) { + if(plugin.build) { + plugin.build(); + } + } + try { + fs.rmSync(this.outputFolder, {recursive: true}); + } catch {} + + const sourceFiles = fs.readdirSync(this.inputFolder, { + recursive: true, + withFileTypes: true, + }); + const globalPlugins = this.plugins.filter((z) => + z.rewriteTriggers.includes("*") + ); + + for await (const file of sourceFiles) { + if (!file.isFile()) continue; + + const type = file.name.split(".").at(-1); + if (!type) continue; + + const shortname = file.name.slice( + 0, + file.name.length - (type.length + 1) + ); + const availablePlugins = this.plugins.filter((z) => + z.rewriteTriggers.includes(type) + ); + console.log(availablePlugins) + + if (availablePlugins.length == 0) { + const oldPath = path.join(file.parentPath, file.name); + fs.cpSync( + oldPath, + oldPath.replace(this.inputFolder, this.outputFolder) + ); + } + + for await (const plugin of availablePlugins) { + const oldPath = path.join(file.parentPath, file.name); + const newPath = path + .join( + file.parentPath, + shortname + + "." + + (plugin.rewriteTriggers.includes("*") ? type : plugin.renameTo) + ) + .replace(this.inputFolder, this.outputFolder); + let data = fs.readFileSync(oldPath).toString("utf8"); + + for await (const globalPlugin of globalPlugins) { + const rewritten = await globalPlugin.rewriteFile(data, oldPath); + if (!rewritten) continue; + data = rewritten; + } + + let rewrite = await plugin.rewriteFile(data, oldPath); + + if (!rewrite) continue; + + fs.mkdirSync(path.dirname(newPath), { recursive: true }); + fs.writeFileSync(newPath, rewrite); + } + } + } +} diff --git a/src/plugins/dev.ts b/src/plugins/dev.ts new file mode 100644 index 0000000..f027aba --- /dev/null +++ b/src/plugins/dev.ts @@ -0,0 +1,98 @@ +import type { Server, ServerWebSocket } from "bun"; +import SSSG, { Plugin } from ".."; +import * as fs from "fs"; +import mime from "mime-types"; + +const script = `let reconnectTimeout;function connect(){console.log("[--dev] connecting to dev server");let ws=new WebSocket("ws://localhost:8080");ws.addEventListener("message",message=>{if(message.data=="refresh"){location.reload()}});ws.addEventListener("open",()=>{console.log("[--dev] connected")});ws.addEventListener("close",()=>{console.log("[--dev] socket closed, restarting in 1s");clearTimeout(reconnectTimeout);reconnectTimeout=setTimeout(()=>{connect()},1e3)})}window.addEventListener("load",()=>connect());`; + +export default class DevPlugin extends Plugin { + build: undefined; + + name = "dev"; + rewriteTriggers = []; + renameTo = undefined; + longLasting = true; + server!: Server; + allConnections: ServerWebSocket[] = []; + + constructor(sssg: SSSG) { + super(); + if (!process.argv.includes("--dev")) return; + + fs.watch( + sssg.inputFolder, + { + recursive: true, + }, + async (e, f) => { + console.log("[dev] Noticed update in " + f + ", of type " + e + "."); + this.allConnections.forEach((z) => z.send("refresh")); + await sssg.build(); + } + ); + + this.server = Bun.serve({ + fetch(req, server) { + const success = server.upgrade(req); + if (success) { + return undefined; + } + + const url = new URL(req.url); + + let cleanedPath = url.pathname; + if (cleanedPath == "/") cleanedPath = "/index.html"; + if (cleanedPath.endsWith("/")) cleanedPath = cleanedPath.slice(0, -1); + + let fsPath = sssg.outputFolder + cleanedPath; + + if (fsPath.match(/\.\.\//g) !== null) { + return undefined; + } + let rawFile; + try { + rawFile = fs.readFileSync(fsPath); + } catch { + return new Response("404 Not Found", { + status: 404, + }); + } + const type = fsPath.split(".").at(-1); + if (!type) return; + if (type == "html") { + + rawFile = rawFile.toString().replace( + "", + `` + ); + } + return new Response(rawFile, { + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + "Content-Type": (mime.lookup(type) || "application/octet-stream") + "; charset=utf-8", + }, + }); + }, + websocket: { + open: (ws) => { + ws.data = Math.random(); + + this.allConnections.push(ws); + }, + message(ws, message) {}, + close: (ws) => { + this.allConnections = this.allConnections.filter( + (z) => z.data != ws.data + ); + }, + }, + port: 8080, + }); + } + + async rewriteFile(file: string, filePath: string) { + return undefined; + } +} diff --git a/src/plugins/markdown-compiler.ts b/src/plugins/markdown-compiler.ts new file mode 100644 index 0000000..63bce9d --- /dev/null +++ b/src/plugins/markdown-compiler.ts @@ -0,0 +1,24 @@ +import { Plugin } from ".."; +import { marked } from "marked"; +import { parseMetadata } from "./markdown-metadata"; + +export default class MarkdownCompiler extends Plugin { + build: undefined; + + name = "markdown-compiler"; + rewriteTriggers = ["md"] + renameTo = "html" + longLasting = false; + + async rewriteFile(file: string, filePath: string) { + let text = file; + const metadata = parseMetadata(text); + if(metadata) { + let textSplit = text.split('\n'); + textSplit.splice(0, Object.keys(metadata).length); + textSplit.unshift(metadata.title) + text = textSplit.join("\n"); + } + return await marked.parse(text); + } +} \ No newline at end of file diff --git a/src/plugins/markdown-metadata.ts b/src/plugins/markdown-metadata.ts new file mode 100644 index 0000000..655246f --- /dev/null +++ b/src/plugins/markdown-metadata.ts @@ -0,0 +1,31 @@ +import { Plugin } from ".."; + +export function parseMetadata(file: string) { + if (!/^=+$/gm.test(file)) return; + const splitfile = file.split("\n"); + let properties: Record | undefined; + for (let i = 0; i < splitfile.length; i++) { + if (!properties) properties = {}; + const line = splitfile[i]; + if (/^=+$/gm.test(line)) break; + const parts = line.split("="); + if (parts.length !== 2) break; + properties[parts[0].trim()] = parts[1].trim(); + } + return properties; +} + +export default class MarkdownMetadataGenerator extends Plugin { + build: undefined; + + name = "markdown-metadata"; + rewriteTriggers = ["md"]; + renameTo = "json"; + longLasting = false; + + async rewriteFile(file: string, filePath: string) { + const metadata = parseMetadata(file); + if (!metadata) return; + return JSON.stringify(metadata); + } +} diff --git a/src/plugins/ts-compiler.ts b/src/plugins/ts-compiler.ts new file mode 100644 index 0000000..fa62fdf --- /dev/null +++ b/src/plugins/ts-compiler.ts @@ -0,0 +1,58 @@ +import { Plugin } from ".."; +import * as fs from "fs"; +import * as esbuild from "esbuild"; + +export default class TSCompiler extends Plugin { + build: undefined; + + name = "ts-compiler"; + rewriteTriggers = ["ts", "tsx", "jsx"]; + renameTo = "js"; + longLasting = false; + + minify = false; + constructor() { + super(); + if (process.argv.includes("--prod")) { + this.minify = true; + } + } + async rewriteFile(file: string, filePath: string) { + let result; + try { + result = await esbuild.build({ + stdin: { + contents: file, + resolveDir: filePath.split("/")?.slice(0, -1).join("/"), + sourcefile: filePath.split("/").at(-1), + loader: filePath.split("/").at(-1)?.split(".").at(-1) as + | "ts" + | "tsx" + | "jsx", + }, + jsxFragment: "Fragment", + jsxFactory: "h", + jsxImportSource: "preact", + jsx: "transform", + write: false, + bundle: true, + outdir: "out", + minify: this.minify, + }); + } catch (e) { + console.error(e); + console.log("Errored!"); + return; + } + + if (result.errors.length != 0) { + console.log("TS compiler errored."); + result.errors.forEach((element) => { + console.error(element); + }); + } else { + const output = result.outputFiles[0].contents; + return new TextDecoder().decode(output); + } + } +} diff --git a/src/plugins/variables.ts b/src/plugins/variables.ts new file mode 100644 index 0000000..f3f8334 --- /dev/null +++ b/src/plugins/variables.ts @@ -0,0 +1,29 @@ +import { Plugin } from ".."; + +export default class Variables extends Plugin { + + name = "variables"; + rewriteTriggers = ["html", "*"]; + renameTo = undefined; + longLasting = false; + + varBuild!: () => Record; + variables!: Record; + + constructor(varBuild: () => Record) { + super(); + this.varBuild = varBuild; + } + + build() { + this.variables = this.varBuild(); + } + + async rewriteFile(file: string, filePath: string): Promise { + let prevfile = file; + for (const a of Object.entries(this.variables)) { + prevfile = prevfile.replaceAll(a[0], a[1]); + } + return prevfile; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}