diff --git a/org-profile-README.md b/org-profile-README.md new file mode 100644 index 0000000..5f019ed --- /dev/null +++ b/org-profile-README.md @@ -0,0 +1,80 @@ +
+ +# Arhitekton + +**Building open-source tools for developers** + +[![GitHub Org](https://img.shields.io/badge/GitHub-Arhitekton-181717?style=flat&logo=github)](https://github.com/Arhitekton) + +
+ +--- + +## About + +Arhitekton is an open-source organization focused on building tools that make developers' lives easier. We work across three areas: + +- **Open-source tooling** — developer utilities, CLI tools, and libraries you can use and contribute to +- **Web applications** — full-stack web projects built with modern technologies +- **Mobile applications** — cross-platform mobile experiences + +We believe good tooling should be free, accessible, and community-driven. + +--- + +## Projects + +| Project | Description | Status | +|---|---|---| +| [Code Visualizer](https://github.com/Arhitekton/GitCodeVisualizer) | Analyze source code and generate interactive dependency graphs | 🚧 Active | + +> More projects coming soon. + +--- + +## Contributors + + + + + + +
+ + millareskenneth
+ millareskenneth +

+ Fullstack Developer +
+ + kimalfredmolina
+ kimalfredmolina +

+ Fullstack Developer +
+ +--- + +## Contributing + +We welcome contributions of all kinds — bug fixes, features, documentation, or ideas. + +1. Browse open issues across our repositories +2. Fork the repo and create a branch (`feat/your-feature`) +3. Open a pull request with a clear description + +Please follow the contribution guidelines in each project's `CONTRIBUTING.md`. + +--- + +## License + +All projects under Arhitekton are open-source and released under the [MIT License](https://opensource.org/licenses/MIT) unless stated otherwise. + +--- + +
+ +*Built with care by the Arhitekton team* + +
diff --git a/package-lock.json b/package-lock.json index 3c6a340..b3766d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "commander": "13.1.0", + "open": "^10.1.0", "typescript": "5.8.3" }, "bin": { @@ -2084,6 +2085,21 @@ "node": ">=8" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -2263,6 +2279,46 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2809,6 +2865,21 @@ "node": ">=0.8.19" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2832,6 +2903,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2842,6 +2931,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3464,6 +3568,24 @@ "node": ">=12.20.0" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3823,6 +3945,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index ccdcf1a..33df1fe 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "packageManager": "npm@10.9.2", "dependencies": { "commander": "13.1.0", + "open": "^10.1.0", "typescript": "5.8.3" }, "devDependencies": { diff --git a/src/cli.ts b/src/cli.ts index 7f3c506..eec96f5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import { Command } from 'commander' import { VERSION } from './index.js' import { analyze } from './analyze.js' import { renderToTerminal } from './renderer.js' +import { serveGraph } from './serve.js' import { generateHtmlReport } from './html-report.js' const program = new Command() @@ -12,25 +13,32 @@ program .description('Analyze source code and generate interactive dependency graphs') .version(VERSION) .argument('[directory]', 'source directory to analyze', '.') - .option('--html', 'generate an interactive HTML report in the report/ directory') - .option('--out-dir ', 'output directory for the HTML report', 'report') + .option('--html', 'open an interactive graph in the browser (live server, no files written)') + .option('--save', 'write the HTML report to disk instead of serving it') + .option('--out-dir ', 'output directory when using --save', 'report') + .option('--port ', 'port for the live server (default: 4242)', '4242') .option('--show-external', 'include external npm packages in the output') .option('--ignore ', 'comma-separated list of directory names to ignore', '') .action(async ( directory: string, - options: { html?: boolean; outDir?: string; showExternal?: boolean; ignore?: string }, + options: { html?: boolean; save?: boolean; outDir?: string; port?: string; showExternal?: boolean; ignore?: string }, ) => { const ignore = options.ignore ? options.ignore.split(',').map((d) => d.trim()).filter(Boolean) : [] try { const graph = await analyze(directory, { ignore }) - if (options.html) { + if (options.save) { const { htmlFile, jsonFile } = await generateHtmlReport(graph, { outDir: options.outDir }) - console.log(`\n HTML report generated:`) + console.log(`\n HTML report saved:`) console.log(` ${htmlFile}`) console.log(` ${jsonFile}`) console.log(`\n Open in browser: file://${htmlFile}\n`) + } else if (options.html) { + const port = parseInt(options.port ?? '4242', 10) + const url = await serveGraph(graph, { port }) + console.log(`\n Serving graph at ${url}`) + console.log(` Press Ctrl+C to stop.\n`) } else { renderToTerminal(graph, { showExternal: options.showExternal }) } diff --git a/src/html-report.ts b/src/html-report.ts index 94cf7c3..39382ef 100644 --- a/src/html-report.ts +++ b/src/html-report.ts @@ -37,7 +37,7 @@ interface SerializedGraph { cycles: string[][] } -function serialize(graph: DependencyGraph): SerializedGraph { +export function serializeGraph(graph: DependencyGraph): SerializedGraph { return { nodes: [...graph.nodes.values()].map((n) => ({ id: n.id, @@ -58,7 +58,7 @@ function serialize(graph: DependencyGraph): SerializedGraph { // HTML template // --------------------------------------------------------------------------- -function buildHtml(title: string, graphJson: string): string { +export function buildHtml(title: string, graphJson: string): string { return /* html */ ` @@ -603,7 +603,7 @@ export async function generateHtmlReport( await mkdir(outDir, { recursive: true }) - const serialized = serialize(graph) + const serialized = serializeGraph(graph) const graphJson = JSON.stringify(serialized, null, 2) const jsonFile = join(outDir, 'graph.json') diff --git a/src/index.ts b/src/index.ts index e10bfed..6ae5137 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export { extractDependencies } from './extractor.js' export { buildGraph } from './graph.js' export { renderToTerminal } from './renderer.js' export { generateHtmlReport } from './html-report.js' +export { serveGraph } from './serve.js' export type { AnalyzeOptions } from './analyze.js' export type { ScanOptions } from './scanner.js' @@ -15,3 +16,4 @@ export type { Dependency } from './extractor.js' export type { DependencyGraph, GraphNode, GraphEdge } from './graph.js' export type { RenderOptions } from './renderer.js' export type { HtmlReportOptions, HtmlReportResult } from './html-report.js' +export type { ServeOptions } from './serve.js' diff --git a/src/serve.ts b/src/serve.ts new file mode 100644 index 0000000..358ba86 --- /dev/null +++ b/src/serve.ts @@ -0,0 +1,60 @@ +import { createServer } from 'node:http' +import type { AddressInfo } from 'node:net' +import type { DependencyGraph } from './graph.js' +import { serializeGraph, buildHtml } from './html-report.js' + +export interface ServeOptions { + /** Preferred port — falls back to a random free port if taken. Defaults to 4242. */ + port?: number + /** Page title */ + title?: string +} + +/** + * Starts an in-process HTTP server that serves the interactive graph report, + * opens the browser automatically, and keeps running until the user hits Ctrl+C. + * + * Returns the bound URL. + */ +export async function serveGraph(graph: DependencyGraph, options: ServeOptions = {}): Promise { + const title = options.title ?? 'codeviz — Dependency Graph' + const graphJson = JSON.stringify(serializeGraph(graph)) + const html = buildHtml(title, graphJson) + + const server = createServer((_req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(html) + }) + + const port = await listen(server, options.port ?? 4242) + const url = `http://localhost:${port}` + + // Dynamic import so the ESM-only `open` package works inside a CJS bundle + const { default: open } = await import('open') + await open(url) + + process.on('SIGINT', () => { + server.close() + process.exit(0) + }) + + return url +} + +/** Binds the server to `preferredPort`, retrying on a random port if EADDRINUSE. */ +function listen(server: ReturnType, preferredPort: number): Promise { + return new Promise((resolve, reject) => { + server.once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + server.listen(0, '127.0.0.1', () => { + resolve((server.address() as AddressInfo).port) + }) + } else { + reject(err) + } + }) + server.listen(preferredPort, '127.0.0.1', () => { + resolve((server.address() as AddressInfo).port) + }) + }) +}