first commit

This commit is contained in:
Soph :3 2024-08-02 04:25:05 +03:00
commit 0ef48e85f0
Signed by: sophie
GPG key ID: EDA5D222A0C270F2
11 changed files with 580 additions and 0 deletions

175
.gitignore vendored Normal file
View file

@ -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

13
README.md Normal file
View file

@ -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

BIN
bun.lockb Executable file

Binary file not shown.

19
package.json Normal file
View file

@ -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"
}
}

106
src/index.ts Normal file
View file

@ -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<string | undefined | null | void>;
}
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);
}
}
}
}

98
src/plugins/dev.ts Normal file
View file

@ -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<number>[] = [];
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<number>({
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(
"<head>",
`<head><script>${script}</script>`
);
}
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;
}
}

View file

@ -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);
}
}

View file

@ -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<string, string> | 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);
}
}

View file

@ -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);
}
}
}

29
src/plugins/variables.ts Normal file
View file

@ -0,0 +1,29 @@
import { Plugin } from "..";
export default class Variables extends Plugin {
name = "variables";
rewriteTriggers = ["html", "*"];
renameTo = undefined;
longLasting = false;
varBuild!: () => Record<string, string>;
variables!: Record<string, string>;
constructor(varBuild: () => Record<string, string>) {
super();
this.varBuild = varBuild;
}
build() {
this.variables = this.varBuild();
}
async rewriteFile(file: string, filePath: string): Promise<string> {
let prevfile = file;
for (const a of Object.entries(this.variables)) {
prevfile = prevfile.replaceAll(a[0], a[1]);
}
return prevfile;
}
}

27
tsconfig.json Normal file
View file

@ -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
}
}