Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 0 additions & 77 deletions CONTRIBUTING.md

This file was deleted.

88 changes: 65 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,77 @@
<samp>

# 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 |
| -------- | ---------------------------------------- |
| linux | `~/.local/share/voidend/` |
| macOS | `~/Library/Application Support/voidend/` |
| windows | `%APPDATA%\voidend\` |

## structure
## Project Structure

```
src/
Expand All @@ -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

</samp>
| 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 |
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function Button({
return (
<ButtonPrimitive
data-slot="button"
suppressHydrationWarning
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
Expand Down
48 changes: 18 additions & 30 deletions src/lib/component-templates/codegen-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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[],
Expand All @@ -54,31 +48,28 @@ 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,
itemTypeName: 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 `function use${hookName}Data() {
const [data, setData] = useState<${itemTypeName}[] | ${itemTypeName} | undefined>(undefined);
Expand Down Expand Up @@ -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');
Expand All @@ -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;
Expand Down
10 changes: 0 additions & 10 deletions src/lib/component-templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading