From e48b2d837d12176036487880ae0bc16a29f567e3 Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Thu, 23 Oct 2025 17:39:31 +0200 Subject: [PATCH] first commit --- .gitignore | 34 +++++++ CLAUDE.md | 106 +++++++++++++++++++++ README.md | 15 +++ bun.lock | 29 ++++++ foo.cs | 9 ++ index.ts | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 12 +++ tsconfig.json | 29 ++++++ 8 files changed, 489 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 foo.cs create mode 100755 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ee6890 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6fff9f --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# csharpierd + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f96c048 --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "csharpierd", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + + "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/foo.cs b/foo.cs new file mode 100644 index 0000000..594879f --- /dev/null +++ b/foo.cs @@ -0,0 +1,9 @@ +namespace CSharpier.Cli.Server; + +public class FormatFileParameter +{ +#pragma warning disable IDE1006 + public required string fileContents { get; set; } + public required string fileName { get; set; } +#pragma warning restore IDE1006 +} diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..98fd218 --- /dev/null +++ b/index.ts @@ -0,0 +1,255 @@ +#!/usr/bin/env bun + +const STATE_FILE = "/tmp/csharpierd-state.json"; +const LOCK_FILE = "/tmp/csharpierd.lock"; +const SERVER_PORT = 18912; +const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour + +interface ServerState { + pid: number; + port: number; + lastAccess: number; +} + +// Acquire lock to prevent race conditions +async function acquireLock(): Promise { + try { + const lockFile = Bun.file(LOCK_FILE); + if (await lockFile.exists()) { + // Check if lock is stale (older than 10 seconds) + const stat = await Bun.file(LOCK_FILE).stat(); + if (Date.now() - stat.mtime.getTime() > 10000) { + await Bun.$`rm -f ${LOCK_FILE}`; + } else { + return false; + } + } + await Bun.write(LOCK_FILE, String(process.pid)); + return true; + } catch { + return false; + } +} + +async function releaseLock(): Promise { + await Bun.$`rm -f ${LOCK_FILE}`.quiet(); +} + +// Load server state +async function loadState(): Promise { + try { + const file = Bun.file(STATE_FILE); + if (!(await file.exists())) return null; + return await file.json(); + } catch { + return null; + } +} + +// Save server state +async function saveState(state: ServerState): Promise { + await Bun.write(STATE_FILE, JSON.stringify(state, null, 2)); +} + +// Check if process is running +async function isProcessRunning(pid: number): Promise { + try { + const result = await Bun.$`kill -0 ${pid}`.quiet(); + return result.exitCode === 0; + } catch { + return false; + } +} + +// Check if server is responsive +async function isServerResponsive(port: number): Promise { + try { + const response = await fetch(`http://localhost:${port}/`, { + signal: AbortSignal.timeout(2000), + }); + return response.ok || response.status === 404; // Server is up if it responds at all + } catch { + return false; + } +} + +// Kill server process +async function killServer(pid: number): Promise { + try { + await Bun.$`kill ${pid}`.quiet(); + // Wait a bit and force kill if needed + await Bun.sleep(500); + if (await isProcessRunning(pid)) { + await Bun.$`kill -9 ${pid}`.quiet(); + } + } catch { + // Ignore errors + } +} + +// Start CSharpier server +async function startServer(): Promise { + console.error("Starting CSharpier server..."); + + // Start server in background + const proc = Bun.spawn( + ["dotnet", "csharpier", "server", "--server-port", String(SERVER_PORT)], + { + stdout: "inherit", + stderr: "inherit", + }, + ); + + const pid = proc.pid; + proc.unref(); // Allow parent to exit without waiting + + // Wait for server to be ready (max 10 seconds) + for (let i = 0; i < 50; i++) { + await Bun.sleep(200); + if (await isServerResponsive(SERVER_PORT)) { + console.error(`CSharpier server started with PID ${pid}`); + return pid; + } + } + + throw new Error("Server failed to start within timeout"); +} + +// Cleanup idle servers +async function cleanupIdleServer(state: ServerState): Promise { + const idleTime = Date.now() - state.lastAccess; + if (idleTime > IDLE_TIMEOUT_MS) { + console.error( + `Server idle for ${Math.floor(idleTime / 1000)}s, shutting down...`, + ); + await killServer(state.pid); + await Bun.$`rm -f ${STATE_FILE}`.quiet(); + } +} + +// Ensure server is running +async function ensureServer(): Promise { + // Try to acquire lock with retries + for (let i = 0; i < 5; i++) { + if (await acquireLock()) break; + await Bun.sleep(100); + } + + try { + let state = await loadState(); + + // Check if we have a running server + if (state) { + // Check idle timeout + await cleanupIdleServer(state); + + // Verify server is still running and responsive + if ( + (await isProcessRunning(state.pid)) && + (await isServerResponsive(state.port)) + ) { + return state; + } else { + console.error( + "Server process not found or not responsive, restarting...", + ); + if (await isProcessRunning(state.pid)) { + await killServer(state.pid); + } + } + } + + // Start new server + const pid = await startServer(); + state = { + pid, + port: SERVER_PORT, + lastAccess: Date.now(), + }; + await saveState(state); + return state; + } finally { + await releaseLock(); + } +} + +interface FormatResult { + formattedFile?: string; + errorMessage?: string; + status: "Formatted" | "Ignored" | "Failed" | "UnsupportedFile"; +} + +// Format code +async function formatCode( + fileName: string, + fileContents: string, +): Promise { + const state = await ensureServer(); + + try { + const response = await fetch(`http://localhost:${state.port}/format`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fileName: `/tmp/${fileName}`, + fileContents, + }), + }); + + if (!response.ok) { + throw new Error( + `Server returned ${response.status}: ${await response.text()}`, + ); + } + + const result = (await response.json()) as FormatResult; + + // Update last access time + state.lastAccess = Date.now(); + await saveState(state); + + if (!result.formattedFile) { + throw new Error(result.errorMessage); + } + + return result.formattedFile; + } catch (error) { + console.error("Error formatting code:", error); + throw error; + } +} + +// Main +async function main() { + const fileName = process.argv[2]; + if (!fileName) { + console.error("Usage: bun index.ts < input.cs"); + process.exit(1); + } + + // Read stdin + const reader = process.stdin; + const chunks: Buffer[] = []; + + for await (const chunk of reader) { + chunks.push(chunk); + } + + const fileContents = Buffer.concat(chunks).toString("utf-8"); + + if (!fileContents) { + console.error("Error: No input provided via stdin"); + process.exit(1); + } + + // Format and output + const formatted = await formatCode(fileName, fileContents); + process.stdout.write(formatted); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c962be7 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "csharpierd", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "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, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}