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