diff --git a/packages/samengine-build/src/buildconfig.ts b/packages/samengine-build/src/buildconfig.ts index fc87053..3d1b70a 100644 --- a/packages/samengine-build/src/buildconfig.ts +++ b/packages/samengine-build/src/buildconfig.ts @@ -34,6 +34,9 @@ export interface buildconfig { devMode: profile; releaseMode: profile; + + // TODO + sfb: SingleFileBundlerConfig; } // Samegui Settigs @@ -80,6 +83,7 @@ export function new_buildconfig(): buildconfig { enable_mobile_css: false, devMode: newDevProfile(), releaseMode: newReleaseProfile(), + sfb: newSingleFileBundlerConfig(), } } @@ -185,3 +189,15 @@ export function newReleaseProfile(): profile { singlefile: false, } } + +// SFB +// SingleFileBundler Config +export interface SingleFileBundlerConfig { + active: boolean; +} + +export function newSingleFileBundlerConfig(): SingleFileBundlerConfig { + return { + active: false + } +} diff --git a/packages/samengine-build/src/cli/cli.ts b/packages/samengine-build/src/cli/cli.ts index 4455cb3..5c75a75 100644 --- a/packages/samengine-build/src/cli/cli.ts +++ b/packages/samengine-build/src/cli/cli.ts @@ -11,7 +11,7 @@ import { WebSocket, WebSocketServer } from "ws"; import { createProject } from "./new.js"; import { copyFolder, flog, getContentType, scanResourcesAsDataURIs, filterResourcesByUsage } from "../buildhelper.js"; -import { GetDefaultHTML, GetSingleFileHTML } from "../exporthtml.js"; +import { GetDefaultHTML, getSingleFileBundlerHTML, GetSingleFileHTML } from "../exporthtml.js"; import { loadUserConfig } from "./config.js"; import { compressHTML } from "../utils/utils.js"; import { parseArgs } from "./argparser.js"; @@ -65,7 +65,40 @@ function createBuilder(config: buildconfig, isRelease: boolean) { await rm(`./${config.outdir}/main.js`, { recursive: true, force: true }); flog("✅ Single-file export created!"); - } else { + } + + // + else if (config.sfb.active) { + // if (isRelease) + + // Static File Bundler + + const bundledJsPath = path.join(".", config.outdir, `${config.entryname.replace(/\.[^.]*$/, "")}.js`); + const bundledJsContent = await readFile(bundledJsPath, "utf-8"); + + // Scan resources and convert to data URIs + // let resourcesMap = await scanResourcesAsDataURIs("./resources"); + + // Filter resources by usage in the bundled code + // resourcesMap = filterResourcesByUsage(bundledJsContent, resourcesMap); + + let html = getSingleFileBundlerHTML(config, bundledJsContent); + if (isRelease) html = await compressHTML(html); + + // Add comment at the beginning after minification + const htmlComment = `\n`; + html = htmlComment + html; + + await writeFile(`./${config.outdir}/index.html`, html); + + // Delete the JS File + await rm(`./${config.outdir}/main.js`, { recursive: true, force: true }); + + flog("✅ Single-file export created!"); + } + + // Esle + else { // Multi-file export (original behavior) let html = GetDefaultHTML(config, isRelease); if (isRelease) html = await compressHTML(html); @@ -174,9 +207,13 @@ async function main() { process.exit(0); } - const config = await loadUserConfig(); + const config: buildconfig = await loadUserConfig(); let builder = createBuilder(config, args.release); + if (!config.enable_audio) { + flog("[INFO]: Audio is disabled!"); + } + let isBuilding = false; let pendingRestart = false; @@ -191,7 +228,7 @@ async function main() { pendingRestart = false; try { // Load the New Config - const newConfig = await loadUserConfig(); + const newConfig: buildconfig = await loadUserConfig(); // Dev Server Stoppen devServer?.stop(); diff --git a/packages/samengine-build/src/cli/config.ts b/packages/samengine-build/src/cli/config.ts index d9f04f9..954117a 100644 --- a/packages/samengine-build/src/cli/config.ts +++ b/packages/samengine-build/src/cli/config.ts @@ -2,8 +2,9 @@ import { build } from "esbuild"; import path from "path"; import { pathToFileURL } from "url"; import fs from "fs/promises"; +import { buildconfig } from "../buildconfig"; -export async function loadUserConfig() { +export async function loadUserConfig(): Promise { const root = process.cwd(); const configPath = path.resolve(root, "samengine.config.ts"); diff --git a/packages/samengine-build/src/exporthtml.ts b/packages/samengine-build/src/exporthtml.ts index 9980a93..9879e39 100644 --- a/packages/samengine-build/src/exporthtml.ts +++ b/packages/samengine-build/src/exporthtml.ts @@ -615,3 +615,117 @@ document.getElementById("mdnotes").remove(); return defaulthtml; } + +///////////////////////////////////////////////////////////////////// + +export function getSingleFileBundlerHTML(c: buildconfig, js: string): string { + return ` + + + + Mini TSX Runtime + + + +
+ + + + + +`; +} diff --git a/packages/samengine-build/src/index.ts b/packages/samengine-build/src/index.ts index 7d8fba4..71b4e96 100644 --- a/packages/samengine-build/src/index.ts +++ b/packages/samengine-build/src/index.ts @@ -6,7 +6,9 @@ export type { SameGUI, HTMLMenuSettingOption, HTMLMenuSetting, - HTMLMenu + HTMLMenu, + + SingleFileBundlerConfig } from "./buildconfig.js"; export { @@ -15,5 +17,7 @@ export { svgfile, newHTMLMenu, newDevProfile, - newReleaseProfile + newReleaseProfile, + + newSingleFileBundlerConfig } from "./buildconfig.js"; diff --git a/packages/samengine/package.json b/packages/samengine/package.json index ab1f019..ce04628 100644 --- a/packages/samengine/package.json +++ b/packages/samengine/package.json @@ -20,7 +20,8 @@ "./utils/csv": "./dist/utils/csv/index.js", "./build": "./dist/build/index.js", "./physics": "./dist/physics/index.js", - "./samegui": "./dist/samegui/index.js" + "./samegui": "./dist/samegui/index.js", + "./site/sfb/hashrouter": "./dist/site/sfb/hashrouter/index.js" }, "keywords": [ "game", diff --git a/packages/samengine/src/site/sfb/hashrouter/index.ts b/packages/samengine/src/site/sfb/hashrouter/index.ts new file mode 100644 index 0000000..01b6ca7 --- /dev/null +++ b/packages/samengine/src/site/sfb/hashrouter/index.ts @@ -0,0 +1,28 @@ +export interface Route { + path: string; + component: () => any; +} + +export class HashRouter { + constructor(private routes: Route[]) {} + + getCurrentComponent() { + let path = "/"; + + if (typeof window !== "undefined") { + path = window.location.hash.slice(1) || "/"; + } + + const route = this.routes.find((r) => r.path === path); + + return route?.component ?? (() => "

404

"); + } + + clientScript() { + return ` + window.addEventListener("hashchange", () => { + location.reload(); + }); + `; + } +} diff --git a/test/sfb/build.ts b/test/sfb/build.ts new file mode 100644 index 0000000..e5720d9 --- /dev/null +++ b/test/sfb/build.ts @@ -0,0 +1,129 @@ +import fs from "fs"; +import path from "path"; +import { pathToFileURL } from "url"; +import esbuild from "esbuild"; +import { render } from "./runtime"; + +async function build() { + // console.log("Run build"); + + fs.mkdirSync("dist", { recursive: true }); + + const ssrFile = path.resolve("dist/ssr.mjs"); + await esbuild.build({ + entryPoints: ["game/main.tsx"], + bundle: true, + outfile: ssrFile, + format: "esm", + platform: "browser", + jsx: "transform", + jsxFactory: "jsx", + jsxFragment: "Fragment", + }); + + // console.log("After esbuild"); + + const { default: App } = await import(pathToFileURL(ssrFile).href); + console.log("App imported"); + + const app = App(); + console.log("App rendered"); + + const html = await minifyHtml(` + + + + + Mini SSG + + + + ${render(app)} + + +`); + + fs.writeFileSync("dist/index.html", html); + fs.rmSync("dist/app.js", { force: true }); + // fs.rmSync(ssrFile, { force: true }); + + console.log("built dist/index.html"); +} + +build(); + +async function minifyHtml(html: string) { + const blocks: string[] = []; + + function keep(block: string) { + const token = `__SAMENGINE_KEEP_${blocks.length}__`; + blocks.push(block); + return token; + } + + html = await minifyBlocks( + html, + /]*)>([\s\S]*?)<\/script>/gi, + async (attrs, code) => { + const result = await esbuild.transform(code, { + loader: "js", + minify: true, + target: "es2018", + }); + + return keep(`${result.code.trim()}`); + }, + ); + + html = await minifyBlocks( + html, + /]*)>([\s\S]*?)<\/style>/gi, + async (attrs, css) => { + const result = await esbuild.transform(css, { + loader: "css", + minify: true, + }); + + return keep(`${result.code.trim()}`); + }, + ); + + html = html.replace( + /<(textarea|pre|code)\b([^>]*)>([\s\S]*?)<\/\1>/gi, + (match) => keep(match), + ); + + html = html + .replace(/>\s+<") + .replace(/\s{2,}/g, " ") + .trim(); + + blocks.forEach((block, index) => { + html = html.replace(`__SAMENGINE_KEEP_${index}__`, block); + }); + + return html; +} + +async function minifyBlocks( + html: string, + pattern: RegExp, + minify: (attrs: string, content: string) => Promise, +) { + let output = ""; + let lastIndex = 0; + + for (const match of html.matchAll(pattern)) { + output += html.slice(lastIndex, match.index); + output += await minify(match[1], match[2]); + lastIndex = match.index! + match[0].length; + } + + return output + html.slice(lastIndex); +} diff --git a/test/sfb/bun.lock b/test/sfb/bun.lock new file mode 100644 index 0000000..65a95a9 --- /dev/null +++ b/test/sfb/bun.lock @@ -0,0 +1,88 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "sfb", + "dependencies": { + "@types/bun": "^1.3.14", + "esbuild": "^0.28.0", + "samengine": "file:../../packages/samengine", + "typescript": "^6.0.3", + }, + "devDependencies": { + "@types/node": "^25.9.2", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@25.9.4", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g=="], + + "automatic-md-index": ["automatic-md-index@1.3.6", "", { "bin": { "md-index": "dist/index.js" } }, "sha512-9D+zgYlK77d8uqwWjGYzjpawegVpHNHEhhWPjGLBzuwUVXUVAThFxU1+fS4TK0XKGvgWj0XQ4UE1GT82MJLdcQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + + "samengine": ["samengine@file:../../packages/samengine", { "devDependencies": { "@types/node": "^25.6.0", "automatic-md-index": "^1.3.6", "chalk": "^5.6.2", "typescript": "^6.0.3" } }], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + } +} diff --git a/test/sfb/file-loader.ts b/test/sfb/file-loader.ts new file mode 100644 index 0000000..30b733a --- /dev/null +++ b/test/sfb/file-loader.ts @@ -0,0 +1,15 @@ +import { readFile } from "node:fs/promises"; + +export async function loadFile(path: string): Promise { + // Bun + if (typeof Bun !== "undefined") { + return await Bun.file(path).text(); + } + + // Node + return await readFile(path, "utf8"); +} + +export async function loadRaw(path: string): Promise { + return loadFile(path); +} diff --git a/test/sfb/game/helper.ts b/test/sfb/game/helper.ts new file mode 100644 index 0000000..201beb7 --- /dev/null +++ b/test/sfb/game/helper.ts @@ -0,0 +1,13 @@ +const routes = { + "/": "

Home

Startseite

", + "/about": "

About

Über uns

", +}; + +function renderRoute() { + const path = location.hash.slice(1) || "/"; + document.getElementById("router-view").innerHTML = + routes[path] || "

404

"; +} + +window.addEventListener("hashchange", renderRoute); +renderRoute(); diff --git a/test/sfb/game/main.tsx b/test/sfb/game/main.tsx new file mode 100644 index 0000000..b48f914 --- /dev/null +++ b/test/sfb/game/main.tsx @@ -0,0 +1,21 @@ +import { loadRaw } from "../file-loader"; +import { jsx, setScript } from "../runtime"; + +function App() { + let source = await loadRaw("./helper"); + + return ( +
+ + +
+ + {setScript(source)} +
+ ); +} + +export default App; diff --git a/test/sfb/game/pages/About.tsx b/test/sfb/game/pages/About.tsx new file mode 100644 index 0000000..f63c14e --- /dev/null +++ b/test/sfb/game/pages/About.tsx @@ -0,0 +1,8 @@ +export default function About() { + return ( +
+

About

+

Über uns

+
+ ); +} diff --git a/test/sfb/game/pages/Home.tsx b/test/sfb/game/pages/Home.tsx new file mode 100644 index 0000000..fb74751 --- /dev/null +++ b/test/sfb/game/pages/Home.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

Home

+

Startseite

+
+ ); +} diff --git a/test/sfb/jsx.d.ts b/test/sfb/jsx.d.ts new file mode 100644 index 0000000..428ffd0 --- /dev/null +++ b/test/sfb/jsx.d.ts @@ -0,0 +1,15 @@ +export {}; + +declare global { + namespace JSX { + type Element = any; + + interface ElementChildrenAttribute { + children: {}; + } + + interface IntrinsicElements { + [tagName: string]: any; + } + } +} diff --git a/test/sfb/package-lock.json b/test/sfb/package-lock.json new file mode 100644 index 0000000..35e2b2b --- /dev/null +++ b/test/sfb/package-lock.json @@ -0,0 +1,128 @@ +{ + "name": "sfb", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sfb", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "esbuild": "^0.28.0", + "samengine": "^1.9.0", + "typescript": "^6.0.3" + }, + "devDependencies": { + "@types/node": "^25.9.2" + } + }, + "../../packages/samengine-build": { + "version": "1.9.5", + "extraneous": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "esbuild": "^0.28.0", + "html-minifier-terser": "^7.2.0", + "mime": "^4.1.0", + "samengine": "^1.7.9", + "ws": "^8.20.0" + }, + "bin": { + "samengine-build": "dist/cli/cli.js" + }, + "devDependencies": { + "@types/html-minifier-terser": "^7.0.2", + "@types/node": "^25.5.2", + "@types/ws": "^8.18.1", + "typescript": "^6.0.3" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/samengine": { + "version": "1.9.0", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/sfb/package.json b/test/sfb/package.json new file mode 100644 index 0000000..cc3de09 --- /dev/null +++ b/test/sfb/package.json @@ -0,0 +1,22 @@ +{ + "name": "sfb", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "npx samengine-build", + "build": "npx samengine-build --release", + "bundle": "bun build.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/bun": "^1.3.14", + "esbuild": "^0.28.0", + "samengine": "file:../../packages/samengine", + "typescript": "^6.0.3" + }, + "devDependencies": { + "@types/node": "^25.9.2" + } +} diff --git a/test/sfb/runtime.ts b/test/sfb/runtime.ts new file mode 100644 index 0000000..84654cb --- /dev/null +++ b/test/sfb/runtime.ts @@ -0,0 +1,83 @@ +(globalThis as any).jsx = function (tag: any, props: any, ...children: any[]) { + return { tag, props, children }; +}; + +export function jsx(tag: any, props: any, ...children: any[]) { + const propChildren = props?.children; + + return { + tag, + props: props || {}, + children: children.length + ? children + : propChildren + ? Array.isArray(propChildren) + ? propChildren + : [propChildren] + : [] + }; +} + +export const jsxs = jsx; +export const Fragment = Symbol("Fragment"); + +export function setInnerHTML(html: any) { + return { __html: String(html) }; +} + +export function setScript(code: any) { + return { __script: String(code) }; +} + +export function render(node: any): string { + if (node == null || node === false) return ""; + + if (node.__html != null) { + return String(node.__html); + } + + if (node.__script != null) { + return ``; + } + + if (typeof node === "string" || typeof node === "number") { + return escapeHtml(String(node)); + } + + if (node.tag === Fragment) { + return (node.children || []).map(render).join(""); + } + + if (typeof node.tag === "function") { + return render(node.tag(node.props || {})); + } + + const rawHtml = node.props?.innerHTML; + const attrs = Object.entries(node.props || {}) + .filter(([k]) => k !== "children" && k !== "innerHTML") + .map(([k, v]) => ` ${k}="${escapeAttr(v)}"`) + .join(""); + + const children = rawHtml != null + ? String(rawHtml) + : (node.children || []).map(render).join(""); + + return `<${node.tag}${attrs}>${children}`; +} + +function escapeHtml(str: string) { + return str + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + +function escapeAttr(str: any) { + return String(str) + .replaceAll("&", "&") + .replaceAll('"', """); +} + +function escapeScript(str: string) { + return str.replaceAll("