commit e48b2d837d12176036487880ae0bc16a29f567e3 Author: MasterGordon Date: Thu Oct 23 17:39:31 2025 +0200 first commit 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 + } +}