Skip to content
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 1.0.0 (2026-06-12)


### Bug Fixes

* **ci:** bump to 0.1.1, simplify npm publish, refresh Actions ([56dbd71](https://github.com/xircons/zero-mock/commit/56dbd71568cfe9a495482c82235c9e5f80589bbe))
* force publish to npm after 2fa issue ([20dd17a](https://github.com/xircons/zero-mock/commit/20dd17a15eb1d133ca8269cf468ea4b8979e4500))


### Features

* add chokidar file watcher with debounce for live reload ([e38b7c1](https://github.com/xircons/zero-mock/commit/e38b7c1659d123efd79be430bfed1260ac2d2a82))
* delay, logging, list filters/pagination, watch mode; docs: demo GIF + README ([7a53693](https://github.com/xircons/zero-mock/commit/7a536930abc7b0491d25e2c4ce18e3b3b441a6de))

# 1.0.0 (2026-06-12)


### Bug Fixes

* **ci:** bump to 0.1.1, simplify npm publish, refresh Actions ([56dbd71](https://github.com/xircons/zero-mock/commit/56dbd71568cfe9a495482c82235c9e5f80589bbe))


### Features

* add chokidar file watcher with debounce for live reload ([e38b7c1](https://github.com/xircons/zero-mock/commit/e38b7c1659d123efd79be430bfed1260ac2d2a82))
* delay, logging, list filters/pagination, watch mode; docs: demo GIF + README ([7a53693](https://github.com/xircons/zero-mock/commit/7a536930abc7b0491d25e2c4ce18e3b3b441a6de))
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@ The root must be a JSON **object**. Each property must be an **array** (your “

## Publishing to npm (maintainers)

**Release flow (recommended):** bump **`version`** in `package.json` and `package-lock.json`, commit, and **push to `main`**. [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml) runs automatically, builds, and runs **`npm publish`**. If that version is already on the registry, the job skips publish and succeeds with a notice (no E403). You can still trigger a run manually from the **Actions** tab (**workflow_dispatch**). Avoid **`npm publish` on your machine** for the same version CI will publish, or you will block CI with “already published”.
**Release flow (recommended):** Automated via `semantic-release`. Commits following the conventional commit format (e.g., `feat:`, `fix:`) pushed to `main` will automatically trigger a version bump, changelog generation, and NPM publish via the [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml) workflow. Avoid running `npm publish` on your machine.

1. Use an [npmjs.com](https://www.npmjs.com/) account with **2FA** enabled and permission to publish the **`@xirconsss`** scope (user or org on npm).
2. **GitHub:** repo → **Settings** → **Environments** → **`NPM_TOKEN`** → add secret **`NPM_TOKEN`** (see token steps below). The workflow uses that environment on each run.
3. Bump **`version`**, push to **`main`**, wait for **Publish to npm** to finish. Check with `npm view @xirconsss/zero-mock version`.
3. Push conventional commits to **`main`**, wait for **Publish to npm** to finish. Check with `npm view @xirconsss/zero-mock version`.

**Token on npm (required for CI):**

Expand Down
47 changes: 44 additions & 3 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@xirconsss/zero-mock",
"version": "0.2.3",
"version": "1.0.0",
"description": "Zero-config CLI that generates REST APIs from JSON files",
"license": "MIT",
"keywords": [
Expand Down Expand Up @@ -47,9 +47,11 @@
},
"homepage": "https://github.com/xircons/zero-mock#readme",
"dependencies": {
"chokidar": "^4.0.3",
"commander": "^13.1.0",
"cors": "^2.8.5",
"express": "^4.21.2"
"express": "^4.21.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@commitlint/cli": "^21.0.2",
Expand Down
46 changes: 28 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { watch } from "fs";
import chokidar from "chokidar";
import { Command } from "commander";
import { JsonStore } from "./store/jsonStore";
import { bootstrap } from "./server/bootstrap";
Expand All @@ -9,6 +9,9 @@ type CliOpts = {
port: string;
delay: string;
watch: boolean;
corsOrigin?: string;
corsMethods?: string;
corsCredentials?: boolean;
};

function parseDelayMs(raw: string): number | null {
Expand All @@ -20,31 +23,30 @@ function parseDelayMs(raw: string): number | null {
}

function startFileWatcher(filePath: string): void {
let reloadInFlight = false;
let reloadTimeout: ReturnType<typeof setTimeout> | null = null;

const reload = async (): Promise<void> => {
if (reloadInFlight) {
return;
}
reloadInFlight = true;
try {
await JsonStore.load(filePath);
console.log(`[watch] Reloaded "${filePath}".`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[watch] Could not reload "${filePath}": ${message}`);
} finally {
reloadInFlight = false;
}
};

try {
watch(filePath, { persistent: true }, () => {
void reload();
chokidar
.watch(filePath, { persistent: true, ignoreInitial: true })
.on("change", () => {
if (reloadTimeout) clearTimeout(reloadTimeout);
reloadTimeout = setTimeout(() => void reload(), 100);
})
.on("error", (err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[watch] Watcher error: ${msg}`);
});
console.log(`[watch] Watching "${filePath}" for changes.`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[watch] Failed to watch "${filePath}": ${message}`);
}

console.log(`[watch] Watching "${filePath}" for changes.`);
}

const program = new Command();
Expand All @@ -56,6 +58,9 @@ program
.option("-p, --port <number>", "HTTP port", "3000")
.option("-d, --delay <ms>", "delay each request by this many ms (0 = off)", "0")
.option("-w, --watch", "reload JSON from disk when the file changes", false)
.option("--cors-origin <origins>", "comma-separated list of allowed origins (e.g., http://localhost:3000)", "*")
.option("--cors-methods <methods>", "comma-separated list of allowed HTTP methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
.option("--cors-credentials", "enable CORS credentials (cookies, authorization headers)", false)
.action(async (opts: CliOpts) => {
const port = Number.parseInt(opts.port, 10);
if (Number.isNaN(port) || port < 1 || port > 65535) {
Expand Down Expand Up @@ -83,7 +88,12 @@ program
}

try {
await bootstrap(port, { delayMs });
await bootstrap(port, {
delayMs,
corsOrigin: opts.corsOrigin,
corsMethods: opts.corsMethods,
corsCredentials: opts.corsCredentials,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Failed to start server: ${message}`);
Expand All @@ -96,4 +106,4 @@ program
}
});

program.parse(process.argv);
program.parse(process.argv);
13 changes: 11 additions & 2 deletions src/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
return;
}
const message = err instanceof Error ? err.message : String(err);
res.status(500).json({ error: message });
res.status(500).json({ error: "INTERNAL_SERVER_ERROR", message, statusCode: 500 });
};

export type CreateAppOptions = {
delayMs: number;
corsOrigin?: string;
corsMethods?: string;
corsCredentials?: boolean;
};

function requestLoggingMiddleware(req: Request, res: Response, next: NextFunction): void {
Expand All @@ -34,7 +37,13 @@ function delayMiddleware(delayMs: number) {

export function createApp(options: CreateAppOptions): Application {
const app = express();
app.use(cors());

app.use(cors({
origin: options.corsOrigin && options.corsOrigin !== "*" ? options.corsOrigin.split(",") : "*",
methods: options.corsMethods || "GET,HEAD,PUT,PATCH,POST,DELETE",
credentials: options.corsCredentials || false,
}));

app.use(express.json());
app.use(requestLoggingMiddleware);
app.use(delayMiddleware(options.delayMs));
Expand Down
Loading
Loading