diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 0000000..1a91291 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,46 @@ +name: Lighthouse CI + +on: + pull_request: + branches: + - main + - develop + +jobs: + lighthouse: + name: Lighthouse CI + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate registry + run: node scripts/generateRegistry.js + + - name: Build application + run: npm run build + env: + NEXT_ENV: local + + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@v11 + with: + urls: | + http://localhost:3000/ + budgetPath: ./lighthouse-budget.json + uploadArtifacts: true + temporaryPublicStorage: true + serverBaseUrl: '' + runs: 3 + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7bd82..74270e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,3 @@ -# [1.4.0-develop.2](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.1...v1.4.0-develop.2) (2026-02-28) - - -### Bug Fixes - -* **tools:** implement proper bcrypt generator ([94d19be](https://github.com/betterbugs/dev-tools/commit/94d19be7e4b8d9256557e7668898ec4d6c3ca15c)), closes [#23](https://github.com/betterbugs/dev-tools/issues/23) [#13](https://github.com/betterbugs/dev-tools/issues/13) - -# [1.4.0-develop.1](https://github.com/betterbugs/dev-tools/compare/v1.3.2...v1.4.0-develop.1) (2026-02-28) - - -### Features - -* **ui:** add reusable CopyButton and refactor wordCounter and jsonToTxt ([d5b9e83](https://github.com/betterbugs/dev-tools/commit/d5b9e8333673c5254cf39529a90869b1b741e385)), closes [#17](https://github.com/betterbugs/dev-tools/issues/17) - ## [1.3.2](https://github.com/betterbugs/dev-tools/compare/v1.3.1...v1.3.2) (2026-02-16) diff --git a/app/components/developmentToolsComponent/bcryptGenerator.tsx b/app/components/developmentToolsComponent/bcryptGenerator.tsx index aa5514b..84e0e63 100644 --- a/app/components/developmentToolsComponent/bcryptGenerator.tsx +++ b/app/components/developmentToolsComponent/bcryptGenerator.tsx @@ -1,6 +1,5 @@ "use client"; import React, { useState, useMemo } from "react"; -import bcrypt from 'bcryptjs'; // Custom styles for the range slider const sliderStyles = ` @@ -49,29 +48,28 @@ const BcryptGenerator = () => { // Simple bcrypt-like hash function (for demonstration - not cryptographically secure) const generateHash = async (text: string, rounds: number): Promise => { - return new Promise((resolve, reject) => { - bcrypt.genSalt(rounds, (err, salt) => { - if (err) return reject(err); - bcrypt.hash(text, salt, (err2, hash) => { - if (err2) return reject(err2); - resolve(hash); - }); - }); - }); + // This is a simplified implementation for demo purposes + // In production, you would use a proper bcrypt library + const encoder = new TextEncoder(); + const data = encoder.encode(text + rounds.toString()); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + return `$2b$${rounds}$${hashHex.substring(0, 53)}`; }; // Simple verification function (for demonstration) const verifyHash = async (password: string, hash: string): Promise => { - return new Promise((resolve) => { - try { - bcrypt.compare(password, hash, (err, res) => { - if (err) return resolve(false); - resolve(Boolean(res)); - }); - } catch { - resolve(false); - } - }); + try { + const parts = hash.split('$'); + if (parts.length !== 4 || parts[1] !== '2b') return false; + + const rounds = parseInt(parts[2]); + const generatedHash = await generateHash(password, rounds); + return generatedHash === hash; + } catch { + return false; + } }; const handleGenerateHash = async () => { diff --git a/app/components/developmentToolsComponent/curlToCodeConverter.tsx b/app/components/developmentToolsComponent/curlToCodeConverter.tsx deleted file mode 100644 index 039a945..0000000 --- a/app/components/developmentToolsComponent/curlToCodeConverter.tsx +++ /dev/null @@ -1,738 +0,0 @@ -"use client"; -import React, { useState, useCallback } from "react"; -import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; -import CopyIcon from "../theme/Icon/copyIcon"; -import ReloadIcon from "../theme/Icon/reload"; - -// ─── Types ─────────────────────────────────────────────────────────────────── - -interface ParsedCurl { - url: string; - method: string; - headers: Record; - body: string | null; - auth: { user: string; password: string } | null; - isFormData: boolean; - isJson: boolean; -} - -type Language = - | "js-fetch" - | "js-axios" - | "python-requests" - | "go" - | "nodejs"; - -// ─── Parser ────────────────────────────────────────────────────────────────── - -function tokenizeCurl(input: string): string[] { - const tokens: string[] = []; - let i = 0; - const s = input.trim().replace(/\\\n/g, " ").replace(/\\\r\n/g, " "); - - while (i < s.length) { - // Skip whitespace - while (i < s.length && /\s/.test(s[i])) i++; - if (i >= s.length) break; - - if (s[i] === "'") { - // Single-quoted string - i++; - let tok = ""; - while (i < s.length && s[i] !== "'") { - tok += s[i++]; - } - i++; // closing quote - tokens.push(tok); - } else if (s[i] === '"') { - // Double-quoted string (handle basic escapes) - i++; - let tok = ""; - while (i < s.length && s[i] !== '"') { - if (s[i] === "\\" && i + 1 < s.length) { - i++; - const c = s[i]; - if (c === "n") tok += "\n"; - else if (c === "t") tok += "\t"; - else if (c === "r") tok += "\r"; - else tok += c; - } else { - tok += s[i]; - } - i++; - } - i++; // closing quote - tokens.push(tok); - } else { - // Unquoted token - let tok = ""; - while (i < s.length && !/\s/.test(s[i])) { - tok += s[i++]; - } - tokens.push(tok); - } - } - return tokens; -} - -function parseCurl(raw: string): ParsedCurl | null { - const tokens = tokenizeCurl(raw); - if (!tokens.length) return null; - - // Must start with 'curl' - if (tokens[0].toLowerCase() !== "curl") return null; - - let url = ""; - let method = ""; - const headers: Record = {}; - let body: string | null = null; - let auth: { user: string; password: string } | null = null; - - for (let i = 1; i < tokens.length; i++) { - const tok = tokens[i]; - - if (tok === "-X" || tok === "--request") { - method = tokens[++i]?.toUpperCase() ?? ""; - } else if (tok === "-H" || tok === "--header") { - const hdr = tokens[++i] ?? ""; - const colon = hdr.indexOf(":"); - if (colon !== -1) { - const key = hdr.slice(0, colon).trim(); - const val = hdr.slice(colon + 1).trim(); - headers[key] = val; - } - } else if ( - tok === "-d" || - tok === "--data" || - tok === "--data-raw" || - tok === "--data-binary" || - tok === "--data-ascii" - ) { - body = tokens[++i] ?? ""; - } else if (tok === "-u" || tok === "--user") { - const creds = tokens[++i] ?? ""; - const idx = creds.indexOf(":"); - if (idx !== -1) { - auth = { - user: creds.slice(0, idx), - password: creds.slice(idx + 1), - }; - } else { - auth = { user: creds, password: "" }; - } - } else if (tok === "--json") { - body = tokens[++i] ?? ""; - headers["Content-Type"] = "application/json"; - headers["Accept"] = "application/json"; - } else if (tok === "--form" || tok === "-F") { - // Basic form data support - collect first occurrence - if (!body) body = tokens[++i] ?? ""; - else i++; // skip but don't overwrite - } else if ( - tok === "--compressed" || - tok === "-L" || - tok === "--location" || - tok === "-k" || - tok === "--insecure" || - tok === "-s" || - tok === "--silent" || - tok === "-v" || - tok === "--verbose" || - tok === "-i" || - tok === "--include" - ) { - // Silently ignore these flags - } else if (tok === "-b" || tok === "--cookie") { - const cookie = tokens[++i] ?? ""; - headers["Cookie"] = cookie; - } else if (tok === "-A" || tok === "--user-agent") { - headers["User-Agent"] = tokens[++i] ?? ""; - } else if (tok === "-e" || tok === "--referer") { - headers["Referer"] = tokens[++i] ?? ""; - } else if (!tok.startsWith("-")) { - // Positional arg = URL - if (!url) url = tok; - } - } - - if (!url) return null; - - // Detect method - if (!method) { - method = body ? "POST" : "GET"; - } - - // Detect content type - const contentType = headers["Content-Type"] || headers["content-type"] || ""; - const isJson = - contentType.includes("application/json") || - (body !== null && body.trimStart().startsWith("{")) || - (body !== null && body.trimStart().startsWith("[")); - const isFormData = contentType.includes("application/x-www-form-urlencoded"); - - return { url, method, headers, body, auth, isFormData, isJson }; -} - -// ─── Code Generators ───────────────────────────────────────────────────────── - -function indent(n: number): string { - return " ".repeat(n); -} - -function jsStringify(val: string): string { - return JSON.stringify(val); -} - -function generateJsFetch(p: ParsedCurl): string { - const lines: string[] = []; - const options: string[] = []; - - if (p.method !== "GET") { - options.push(`${indent(1)}method: ${jsStringify(p.method)},`); - } - - const headerEntries = Object.entries(p.headers); - if (p.auth) { - const encoded = Buffer.from( - `${p.auth.user}:${p.auth.password}` - ).toString("base64"); - headerEntries.push(["Authorization", `Basic ${encoded}`]); - } - - if (headerEntries.length) { - options.push(`${indent(1)}headers: {`); - for (const [k, v] of headerEntries) { - options.push(`${indent(2)}${jsStringify(k)}: ${jsStringify(v)},`); - } - options.push(`${indent(1)}},`); - } - - if (p.body !== null) { - if (p.isJson) { - try { - const parsed = JSON.parse(p.body); - options.push( - `${indent(1)}body: JSON.stringify(${JSON.stringify(parsed, null, 2) - .split("\n") - .join("\n" + indent(1))}),` - ); - } catch { - options.push(`${indent(1)}body: ${jsStringify(p.body)},`); - } - } else { - options.push(`${indent(1)}body: ${jsStringify(p.body)},`); - } - } - - lines.push(`const response = await fetch(${jsStringify(p.url)}, {`); - lines.push(...options); - lines.push(`});`); - lines.push(`const data = await response.json();`); - lines.push(`console.log(data);`); - - return lines.join("\n"); -} - -function generateJsAxios(p: ParsedCurl): string { - const lines: string[] = []; - - const configParts: string[] = []; - configParts.push(`${indent(1)}method: ${jsStringify(p.method.toLowerCase())},`); - configParts.push(`${indent(1)}url: ${jsStringify(p.url)},`); - - const headerEntries = Object.entries(p.headers); - if (p.auth) { - lines.unshift(`// Axios handles basic auth natively:`); - configParts.push(`${indent(1)}auth: {`); - configParts.push(`${indent(2)}username: ${jsStringify(p.auth.user)},`); - configParts.push(`${indent(2)}password: ${jsStringify(p.auth.password)},`); - configParts.push(`${indent(1)}},`); - } - - if (headerEntries.length) { - configParts.push(`${indent(1)}headers: {`); - for (const [k, v] of headerEntries) { - configParts.push(`${indent(2)}${jsStringify(k)}: ${jsStringify(v)},`); - } - configParts.push(`${indent(1)}},`); - } - - if (p.body !== null) { - if (p.isJson) { - try { - const parsed = JSON.parse(p.body); - const jsonStr = JSON.stringify(parsed, null, 2) - .split("\n") - .join("\n" + indent(1)); - configParts.push(`${indent(1)}data: ${jsonStr},`); - } catch { - configParts.push(`${indent(1)}data: ${jsStringify(p.body)},`); - } - } else { - configParts.push(`${indent(1)}data: ${jsStringify(p.body)},`); - } - } - - lines.push(`import axios from 'axios';`); - lines.push(``); - lines.push(`const response = await axios({`); - lines.push(...configParts); - lines.push(`});`); - lines.push(`console.log(response.data);`); - - return lines.join("\n"); -} - -function generatePythonRequests(p: ParsedCurl): string { - const lines: string[] = []; - lines.push(`import requests`); - lines.push(``); - - const args: string[] = ["url"]; - - // Headers - const headerEntries = Object.entries(p.headers); - if (headerEntries.length) { - lines.push(`headers = {`); - for (const [k, v] of headerEntries) { - lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`); - } - lines.push(`}`); - lines.push(``); - args.push(`headers=headers`); - } - - // Auth - if (p.auth) { - args.push( - `auth=(${JSON.stringify(p.auth.user)}, ${JSON.stringify(p.auth.password)})` - ); - } - - // Body - if (p.body !== null) { - if (p.isJson) { - try { - const parsed = JSON.parse(p.body); - const pyJson = JSON.stringify(parsed, null, 4) - .replace(/: null/g, ": None") - .replace(/: true/g, ": True") - .replace(/: false/g, ": False"); - lines.push(`payload = ${pyJson}`); - } catch { - lines.push(`payload = ${JSON.stringify(p.body)}`); - } - lines.push(``); - args.push(`json=payload`); - } else if (p.isFormData) { - lines.push(`data = ${JSON.stringify(p.body)}`); - lines.push(``); - args.push(`data=data`); - } else { - lines.push(`data = ${JSON.stringify(p.body)}`); - lines.push(``); - args.push(`data=data`); - } - } - - const methodLower = p.method.toLowerCase(); - lines.push(`url = ${JSON.stringify(p.url)}`); - lines.push(``); - lines.push(`response = requests.${methodLower}(${args.join(", ")})`); - lines.push(`print(response.json())`); - - return lines.join("\n"); -} - -function generateGo(p: ParsedCurl): string { - const lines: string[] = []; - - lines.push(`package main`); - lines.push(``); - lines.push(`import (`); - lines.push(`\t"fmt"`); - if (p.body) lines.push(`\t"strings"`); - lines.push(`\t"net/http"`); - lines.push(`\t"io"`); - lines.push(`)`); - lines.push(``); - lines.push(`func main() {`); - lines.push(`\turl := ${JSON.stringify(p.url)}`); - lines.push(``); - - if (p.body) { - const bodyStr = p.isJson - ? (() => { - try { - return JSON.stringify(JSON.parse(p.body), null, 2); - } catch { - return p.body; - } - })() - : p.body; - lines.push(`\tbody := strings.NewReader(${JSON.stringify(bodyStr)})`); - lines.push( - `\treq, err := http.NewRequest(${JSON.stringify(p.method)}, url, body)` - ); - } else { - lines.push( - `\treq, err := http.NewRequest(${JSON.stringify(p.method)}, url, nil)` - ); - } - - lines.push(`\tif err != nil {`); - lines.push(`\t\tpanic(err)`); - lines.push(`\t}`); - lines.push(``); - - for (const [k, v] of Object.entries(p.headers)) { - lines.push(`\treq.Header.Set(${JSON.stringify(k)}, ${JSON.stringify(v)})`); - } - - if (p.auth) { - lines.push( - `\treq.SetBasicAuth(${JSON.stringify(p.auth.user)}, ${JSON.stringify(p.auth.password)})` - ); - } - - lines.push(``); - lines.push(`\tclient := &http.Client{}`); - lines.push(`\tresp, err := client.Do(req)`); - lines.push(`\tif err != nil {`); - lines.push(`\t\tpanic(err)`); - lines.push(`\t}`); - lines.push(`\tdefer resp.Body.Close()`); - lines.push(``); - lines.push(`\tbody2, _ := io.ReadAll(resp.Body)`); - lines.push(`\tfmt.Println(string(body2))`); - lines.push(`}`); - - return lines.join("\n"); -} - -function generateNodeJs(p: ParsedCurl): string { - const lines: string[] = []; - - try { - const urlObj = new URL(p.url); - const protocol = urlObj.protocol.replace(":", ""); - const hostname = urlObj.hostname; - const port = - urlObj.port || (protocol === "https" ? "443" : "80"); - const path = urlObj.pathname + (urlObj.search || ""); - - lines.push(`const ${protocol} = require('${protocol}');`); - lines.push(``); - - if (p.body) { - const bodyStr = p.isJson - ? (() => { - try { - return JSON.stringify(JSON.parse(p.body), null, 2); - } catch { - return p.body; - } - })() - : p.body; - lines.push(`const postData = ${JSON.stringify(bodyStr)};`); - lines.push(``); - } - - lines.push(`const options = {`); - lines.push(` hostname: ${JSON.stringify(hostname)},`); - if ( - (protocol === "https" && port !== "443") || - (protocol === "http" && port !== "80") - ) { - lines.push(` port: ${port},`); - } - lines.push(` path: ${JSON.stringify(path || "/")},`); - lines.push(` method: ${JSON.stringify(p.method)},`); - - const headerEntries = Object.entries(p.headers); - if (p.body) { - headerEntries.push(["Content-Length", "Buffer.byteLength(postData)"]); - } - if (p.auth) { - const creds = `${p.auth.user}:${p.auth.password}`; - const encoded = Buffer.from(creds).toString("base64"); - headerEntries.push(["Authorization", `Basic ${encoded}`]); - } - - if (headerEntries.length) { - lines.push(` headers: {`); - for (const [k, v] of headerEntries) { - if (k === "Content-Length") { - lines.push(` ${JSON.stringify(k)}: ${v},`); - } else { - lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`); - } - } - lines.push(` },`); - } - - lines.push(`};`); - lines.push(``); - lines.push(`const req = ${protocol}.request(options, (res) => {`); - lines.push(` let data = '';`); - lines.push(` res.on('data', (chunk) => { data += chunk; });`); - lines.push(` res.on('end', () => { console.log(JSON.parse(data)); });`); - lines.push(`});`); - lines.push(``); - lines.push(`req.on('error', (e) => { console.error(e); });`); - - if (p.body) { - lines.push(`req.write(postData);`); - } - - lines.push(`req.end();`); - } catch { - // Fallback if URL parsing fails - lines.push(`// Could not fully parse the URL: ${p.url}`); - lines.push( - `// Please adjust hostname, path and port below.` - ); - lines.push(`const https = require('https');`); - lines.push(`// ... (manual setup required)`); - } - - return lines.join("\n"); -} - -function generateCode(parsed: ParsedCurl, lang: Language): string { - switch (lang) { - case "js-fetch": - return generateJsFetch(parsed); - case "js-axios": - return generateJsAxios(parsed); - case "python-requests": - return generatePythonRequests(parsed); - case "go": - return generateGo(parsed); - case "nodejs": - return generateNodeJs(parsed); - default: - return ""; - } -} - -// ─── Sample cURL commands ──────────────────────────────────────────────────── - -const SAMPLES: Record = { - "GET with headers": `curl https://api.example.com/users \\\n -H 'Authorization: Bearer YOUR_TOKEN' \\\n -H 'Accept: application/json'`, - "POST JSON": `curl -X POST https://api.example.com/users \\\n -H 'Content-Type: application/json' \\\n -H 'Authorization: Bearer YOUR_TOKEN' \\\n -d '{"name":"John Doe","email":"john@example.com"}'`, - "PUT with body": `curl -X PUT https://api.example.com/users/42 \\\n -H 'Content-Type: application/json' \\\n -d '{"name":"Jane Doe"}'`, - "Basic auth": `curl https://api.example.com/secure \\\n -u username:password`, - "Form data": `curl -X POST https://api.example.com/login \\\n -H 'Content-Type: application/x-www-form-urlencoded' \\\n -d 'username=john&password=secret'`, -}; - -const LANGUAGES: { value: Language; label: string }[] = [ - { value: "js-fetch", label: "JavaScript (Fetch)" }, - { value: "js-axios", label: "JavaScript (Axios)" }, - { value: "python-requests", label: "Python (Requests)" }, - { value: "go", label: "Go" }, - { value: "nodejs", label: "Node.js" }, -]; - -// ─── Component ──────────────────────────────────────────────────────────────── - -const CurlToCodeConverter = () => { - const [curlInput, setCurlInput] = useState(""); - const [language, setLanguage] = useState("js-fetch"); - const [output, setOutput] = useState(""); - const [error, setError] = useState(""); - const [copySuccess, setCopySuccess] = useState(false); - - const convert = useCallback( - (raw: string, lang: Language) => { - const trimmed = raw.trim(); - if (!trimmed) { - setOutput(""); - setError(""); - return; - } - const parsed = parseCurl(trimmed); - if (!parsed) { - setOutput(""); - setError( - "Could not parse the cURL command. Make sure it starts with 'curl' and includes a URL." - ); - return; - } - setError(""); - setOutput(generateCode(parsed, lang)); - }, - [] - ); - - const handleInputChange = (e: React.ChangeEvent) => { - const val = e.target.value; - setCurlInput(val); - convert(val, language); - }; - - const handleLanguageChange = (e: React.ChangeEvent) => { - const lang = e.target.value as Language; - setLanguage(lang); - convert(curlInput, lang); - }; - - const handleLoadSample = (sampleKey: string) => { - const sample = SAMPLES[sampleKey]; - if (sample) { - setCurlInput(sample); - convert(sample, language); - } - }; - - const handleCopy = async () => { - if (!output) return; - try { - await navigator.clipboard.writeText(output); - setCopySuccess(true); - setTimeout(() => setCopySuccess(false), 2000); - } catch { - setError("Failed to copy to clipboard."); - } - }; - - const handleClear = () => { - setCurlInput(""); - setOutput(""); - setError(""); - setCopySuccess(false); - }; - - return ( -
-
-
-
-
- {/* Controls row */} -
- {/* Language selector */} -
- - -
- - {/* Samples */} -
- - -
- - {/* Action buttons */} -
- - -
-
- - {/* Editor area */} -
- {/* Input */} -
- -