diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 01af99b..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,77 +0,0 @@ - - -# contributing - -this is a way to contribute in project project so nothing too serious - just follow this so we don't break each other's work. - -## workflow - -**1. pull latest main first** - -``` -git pull origin main -``` - -**2. create a new branch for your changes** - -``` -git switch -c your-branch-name -``` - -**3. make your changes** - -**4. before committing — run this** - -``` -pnpm check -``` - -fix whatever it complains about, then run - -``` -pnpm check:fix -``` - -to auto-fix formatting and lint issues. **don't skip this** — there's a CI pipeline and it will fail if your code has lint or formatting errors. - -**5. commit and push** - -``` -git add . -git commit -m "describe what you changed" -git push origin your-branch-name -``` - -**6. open a pull request on github** to merge into `main` - -**7. ask a team member to review it** - -**8. once approved, merge it on github** - -**9. delete the branch, start fresh for the next change** - -## branch naming - -keep it simple and descriptive - -``` -feat/add-login -fix/db-connection -chore/update-readme -``` - -## commit messages - -use [conventional commits](https://www.conventionalcommits.org) format - -``` -type: short description -``` - -## the rule - -> never push directly to `main` - -always go through a branch + pull request. always. - - diff --git a/README.md b/README.md index 2faf267..55c27d2 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,69 @@ - - # voidend -## setup +Local-first, offline mock API tool for frontend developers. Define schema-driven endpoints, get realistic fake data instantly — no backend, no internet. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Build & Run](#build--run) +- [Features](#features) +- [Writing Schemas](#writing-schemas) +- [Data Directory](#data-directory) +- [Project Structure](#project-structure) +- [Scripts](#scripts) +- [Contributing](#contributing) + +## Quick Start ``` git clone https://github.com/Gr1shma/voidend/ cd voidend pnpm install pnpm db:push +pnpm dev ``` -## dev +## Build & Run ``` -pnpm dev +pnpm build +pnpm start +``` + +Or use `pnpm preview` to build and start in one step. + +## Features + +- Schema-driven mock endpoints with `$faker.*` powered fake data +- Nested objects and repeated arrays via `$array` / `$count` +- Network simulation — delays, failure rates +- Mock JWT authentication (`/login`, `requiresAuth`) +- Export/import projects as JSON +- Everything runs locally in SQLite — no external services + +## Writing Schemas + +`responseSchema` is a JSON object. Strings starting with `$faker.` resolve to fake values via [faker.js](https://fakerjs.dev/). + +```json +{ + "id": "$faker.string.uuid", + "name": "$faker.person.fullName", + "tags": { + "$array": "$faker.lorem.word", + "$count": 3 + } +} ``` -## data directory +- Static values (`"admin"`, `42`, `true`) pass through unchanged. +- `$array` repeats its template `$count` times (default 3). +- Use `responseCount` on the endpoint for top-level list responses — don't combine with a top-level `$array`. +- Invalid faker paths silently resolve to `null`. + +Data is resolved once and cached to disk; editing the schema or count regenerates it. -voidend stores its database and mock data in: +## Data Directory | platform | path | | -------- | ---------------------------------------- | @@ -27,7 +71,7 @@ voidend stores its database and mock data in: | macOS | `~/Library/Application Support/voidend/` | | windows | `%APPDATA%\voidend\` | -## structure +## Project Structure ``` src/ @@ -41,19 +85,17 @@ src/ └── styles/ global css ``` -## scripts - -| command | does | -| ------------------ | ------------------------------- | -| `pnpm dev` | start dev server with turbopack | -| `pnpm check` | lint and format | -| `pnpm check:fix` | lint + autofix + format | -| `pnpm db:generate` | generate migrations | -| `pnpm db:migrate` | apply migrations | -| `pnpm db:push` | push schema changes to db | -| `pnpm db:studio` | open drizzle studio | -| `pnpm typecheck` | run typescript checks | - -contributing? read [CONTRIBUTING.md](./CONTRIBUTING.md) first. +## Scripts - +| command | does | +| ---------------- | ------------------------------- | +| `pnpm dev` | start dev server with turbopack | +| `pnpm build` | build for production | +| `pnpm start` | start production server | +| `pnpm preview` | build and start in one step | +| `pnpm check` | lint and format | +| `pnpm check:fix` | lint + autofix + format | +| `pnpm db:push` | push schema changes to db | +| `pnpm db:studio` | open drizzle studio | +| `pnpm typecheck` | run typescript checks | +| `pnpm test` | run tests | diff --git a/package.json b/package.json index fb2f3a2..324b7ca 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "clsx": "^2.1.1", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.41.0", + "json5": "^2.2.3", "jsonwebtoken": "^9.0.3", "lucide-react": "^1.17.0", "motion": "^12.40.0", @@ -56,6 +57,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4.0.15", "@types/better-sqlite3": "^7.6.13", + "@types/json5": "^2.2.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.14.10", "@types/react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44b9ff4..fdb67cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: drizzle-orm: specifier: ^0.41.0 version: 0.41.0(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1) + json5: + specifier: ^2.2.3 + version: 2.2.3 jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 @@ -102,6 +105,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 + '@types/json5': + specifier: ^2.2.0 + version: 2.2.0 '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 @@ -1637,6 +1643,10 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json5@2.2.0': + resolution: {integrity: sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==} + deprecated: This is a stub types definition. json5 provides its own type definitions, so you do not need this installed. + '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} @@ -4705,6 +4715,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/json5@2.2.0': + dependencies: + json5: 2.2.3 + '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index ef8c667..9563988 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -48,6 +48,7 @@ function Button({ return ( diff --git a/src/lib/component-templates/codegen-utils.ts b/src/lib/component-templates/codegen-utils.ts index 6f3ca3a..2ecc141 100644 --- a/src/lib/component-templates/codegen-utils.ts +++ b/src/lib/component-templates/codegen-utils.ts @@ -28,9 +28,6 @@ export function mapTsType(dt: string): string { return "string"; } -/** - * Finds a field based on matching its faker dataType string. - */ export function findFieldByDataType( fields: SchemaField[], dataTypePatterns: string[], @@ -41,9 +38,6 @@ export function findFieldByDataType( })?.fieldName; } -/** - * Finds all fields matching any of the given faker dataType patterns. - */ export function filterFieldsByDataType( fields: SchemaField[], dataTypePatterns: string[], @@ -54,21 +48,12 @@ export function filterFieldsByDataType( }); } -/** - * Finds the user's actual fieldName matching any of the given candidates - * (case + separator insensitive). Returns null if none found so the - * template can decide on a fallback. - */ export function findField(fields: SchemaField[], candidates: string[]): string | null { const normalizedCandidates = new Set(candidates.map(normalize)); const match = fields.find((f) => normalizedCandidates.has(normalize(f.fieldName))); return match ? match.fieldName : null; } -/** - * Builds a fetch hook that handles both response shapes from the mock - * endpoint: a single object (count === 1) or an array (count > 1). - */ export function buildFetchHook( hookName: string, endpointUrl: string, @@ -76,9 +61,15 @@ export function buildFetchHook( options?: TemplateOptions, ): string { const tokenVal = options?.bearerToken || "YOUR_TOKEN_HERE"; - const fetchArgs = options?.requiresAuth - ? `"${endpointUrl}", {\n headers: {\n "Authorization": "Bearer ${tokenVal}"\n }\n }` - : `"${endpointUrl}"`; + const method = options?.method && options.method !== "GET" ? options.method : undefined; + const fetchOptions: string[] = []; + if (method) fetchOptions.push(`method: "${method}"`); + if (options?.requiresAuth) + fetchOptions.push(`headers: {\n "Authorization": "Bearer ${tokenVal}"\n }`); + const fetchArgs = + fetchOptions.length > 0 + ? `"${endpointUrl}", {\n ${fetchOptions.join(",\n ")}\n }` + : `"${endpointUrl}"`; return `function use${hookName}Data() { const [data, setData] = useState<${itemTypeName}[] | ${itemTypeName} | undefined>(undefined); @@ -106,20 +97,21 @@ ${fieldLines.map((l) => ` ${l}`).join("\n")} }`; } -/** - * Builds a vanilla-JS fetch + DOM-render script block for HTML templates. - * `renderFn` is a JS expression (string) that maps a single item to an - * HTML string — the caller provides the item-level template string. - */ export function buildHtmlFetchScript( endpointUrl: string, renderFn: string, options?: TemplateOptions, ): string { const tokenVal = options?.bearerToken || "YOUR_TOKEN_HERE"; - const fetchArgs = options?.requiresAuth - ? `"${endpointUrl}", {\n headers: {\n "Authorization": "Bearer ${tokenVal}"\n }\n }` - : `"${endpointUrl}"`; + const method = options?.method && options.method !== "GET" ? options.method : undefined; + const fetchOptions: string[] = []; + if (method) fetchOptions.push(`method: "${method}"`); + if (options?.requiresAuth) + fetchOptions.push(`headers: {\n "Authorization": "Bearer ${tokenVal}"\n }`); + const fetchArgs = + fetchOptions.length > 0 + ? `"${endpointUrl}", {\n ${fetchOptions.join(",\n ")}\n }` + : `"${endpointUrl}"`; return ` async function loadData() { const root = document.getElementById('root'); @@ -139,10 +131,6 @@ export function buildHtmlFetchScript( loadData();`; } -/** - * Returns a baseline CSS string used by all HTML templates: - * custom properties for color, card grid, avatar, etc. - */ export function buildHtmlStyles(): string { return ` :root { --bg: #ffffff; diff --git a/src/lib/component-templates/index.ts b/src/lib/component-templates/index.ts index 64bf4b8..a396238 100644 --- a/src/lib/component-templates/index.ts +++ b/src/lib/component-templates/index.ts @@ -30,13 +30,6 @@ function fieldMatches(fieldNameNormalized: string, candidateNormalized: string): ); } -/** - * Scores every template against the given schema fields and endpoint URL path. - * Matches against field $faker dataTypes (and fieldNames as fallback). - * requiredFields matches are weighted heavily (10x); - * path segment matches add a strong boost (8x); - * optionalFields matches add a smaller boost. - */ export function matchTemplates(fields: SchemaField[], endpointPath: string = ""): TemplateMatch[] { const normalizedFieldNames = fields.map((f) => normalize(f.fieldName)); const normalizedDataTypes = fields.map((f) => normalize(f.dataType)); @@ -45,7 +38,6 @@ export function matchTemplates(fields: SchemaField[], endpointPath: string = "") const results: TemplateMatch[] = COMPONENT_TEMPLATES.map((template) => { const matchedFields: string[] = []; - // Endpoint path keyword bonus let pathScoreBoost = 0; const templateKeywords = [ template.id, @@ -95,12 +87,10 @@ export function matchTemplates(fields: SchemaField[], endpointPath: string = "") return { template, score, matchedFields }; }); - // If template has 0 score or only path match without any fields, ensure dynamic-grid is fallback const matched = results .filter((r) => r.score > 0) .toSorted((a: TemplateMatch, b: TemplateMatch) => b.score - a.score); - // If dynamicGrid is not present or score is low, guarantee dynamicGrid is included at the end const dynamicMatch = results.find((r) => r.template.id === "dynamic-grid"); if (dynamicMatch && !matched.some((m: TemplateMatch) => m.template.id === "dynamic-grid")) { matched.push(dynamicMatch); diff --git a/src/lib/component-templates/types.ts b/src/lib/component-templates/types.ts index 49e6498..ef124c1 100644 --- a/src/lib/component-templates/types.ts +++ b/src/lib/component-templates/types.ts @@ -3,19 +3,16 @@ import type { SchemaField } from "~/lib/faker-options"; export interface TemplateOptions { requiresAuth?: boolean; bearerToken?: string | null; + method?: string; } export interface ComponentTemplate { id: string; name: string; description: string; - /** fieldName candidates that strongly indicate this template (case/underscore-insensitive) */ requiredFields: string[]; - /** fieldName candidates that boost match confidence but aren't essential */ optionalFields: string[]; - /** generates the full .tsx source as a string, using the user's actual field names */ code: (fields: SchemaField[], endpointUrl: string, options?: TemplateOptions) => string; - /** generates a standalone .html file (inline