@carlos-tzin/tzin 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +5 -0
- package/dist/cli.js +14 -7
- package/dist/config.d.ts +24 -0
- package/dist/config.js +32 -0
- package/dist/dev-server.js +29 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/route-loader.d.ts +17 -0
- package/dist/route-loader.js +50 -0
- package/dist/test.d.ts +65 -0
- package/dist/test.js +131 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Added `create-tzin` scaffolding CLI (`npx create-tzin my-app`)
|
|
6
|
+
- Templates: Node, Bun, Cloudflare Workers
|
|
7
|
+
- Interactive and non-interactive modes
|
|
8
|
+
- Added API Reference and Architecture Guide docs
|
|
9
|
+
|
|
10
|
+
## 0.1.1
|
|
11
|
+
|
|
12
|
+
- Documentation improvements: trimmed roadmap, added examples table
|
|
13
|
+
|
|
3
14
|
## 0.1.0 — first public release
|
|
4
15
|
|
|
5
16
|
Contract-first TypeScript framework: declare a contract once, get the typed
|
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@ Or scaffold a new project:
|
|
|
20
20
|
npx create-tzin my-app
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
- **[API Reference](docs/api-reference.md)** — all exports, types, and options
|
|
26
|
+
- **[Architecture Guide](docs/architecture.md)** — request pipeline, design decisions, internals
|
|
27
|
+
|
|
23
28
|
## Why another framework?
|
|
24
29
|
|
|
25
30
|
The TypeScript backend landscape is crowded — and still leaves real gaps:
|
package/dist/cli.js
CHANGED
|
@@ -3,21 +3,28 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
function usage() {
|
|
4
4
|
console.error(`tzin CLI
|
|
5
5
|
|
|
6
|
-
tzin dev
|
|
7
|
-
|
|
6
|
+
tzin dev [entry] [--port N] start dev server with hot reload
|
|
7
|
+
entry defaults to src/app.ts`);
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
const [, , cmd, ...rest] = process.argv;
|
|
11
|
-
if (cmd !== 'dev'
|
|
11
|
+
if (cmd !== 'dev')
|
|
12
12
|
usage();
|
|
13
|
-
|
|
13
|
+
// Entry is optional now - dev-server auto-detects
|
|
14
|
+
let entry = '';
|
|
14
15
|
let port = '3000';
|
|
15
16
|
const portFlag = rest.indexOf('--port');
|
|
16
17
|
if (portFlag !== -1 && rest[portFlag + 1])
|
|
17
18
|
port = rest[portFlag + 1];
|
|
19
|
+
// First non-flag arg is the entry (optional)
|
|
20
|
+
const entryArg = rest.find((a) => !a.startsWith('-') && a !== 'dev');
|
|
21
|
+
if (entryArg)
|
|
22
|
+
entry = entryArg;
|
|
18
23
|
const devServer = fileURLToPath(new URL('./dev-server.ts', import.meta.url));
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const args = ['tsx', 'watch', '--clear-screen=false', devServer];
|
|
25
|
+
if (entry)
|
|
26
|
+
args.push(entry);
|
|
27
|
+
args.push('--port', port);
|
|
28
|
+
const child = spawn('npx', args, { stdio: 'inherit' });
|
|
22
29
|
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
23
30
|
child.on('exit', (code) => process.exit(code ?? 0));
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TzinConfig {
|
|
2
|
+
/** Port to listen on (default: 3000) */
|
|
3
|
+
port?: number;
|
|
4
|
+
/** Enable OpenAPI at /openapi.json */
|
|
5
|
+
openapi?: boolean;
|
|
6
|
+
/** Enable MCP at POST /mcp */
|
|
7
|
+
mcp?: boolean;
|
|
8
|
+
/** Enable /llms.txt and /llms-full.txt */
|
|
9
|
+
llms?: boolean;
|
|
10
|
+
/** API metadata for OpenAPI/MCP */
|
|
11
|
+
meta?: {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Glob patterns for auto-loading routes */
|
|
17
|
+
routes?: string[];
|
|
18
|
+
/** Glob patterns for auto-loading middleware */
|
|
19
|
+
middleware?: string[];
|
|
20
|
+
/** Entry point for the app (default: src/app.ts) */
|
|
21
|
+
entry?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function loadConfig(cwd?: string): TzinConfig | null;
|
|
24
|
+
export declare function defineConfig(config: TzinConfig): TzinConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
const CONFIG_FILES = [
|
|
4
|
+
'tzin.config.ts',
|
|
5
|
+
'tzin.config.js',
|
|
6
|
+
'tzin.config.mjs',
|
|
7
|
+
'tzin.config.json',
|
|
8
|
+
];
|
|
9
|
+
export function loadConfig(cwd = process.cwd()) {
|
|
10
|
+
for (const file of CONFIG_FILES) {
|
|
11
|
+
const path = resolve(cwd, file);
|
|
12
|
+
if (existsSync(path)) {
|
|
13
|
+
try {
|
|
14
|
+
// For .ts files, we need to use a loader
|
|
15
|
+
if (file.endsWith('.ts')) {
|
|
16
|
+
// Return null for now - will be handled by tsx loader
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// For .js/.mjs, import dynamically
|
|
20
|
+
const config = require(path);
|
|
21
|
+
return config.default || config;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export function defineConfig(config) {
|
|
31
|
+
return config;
|
|
32
|
+
}
|
package/dist/dev-server.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
2
4
|
import { listen } from './node.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
3
6
|
function pad(s, n) {
|
|
4
7
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
5
8
|
}
|
|
@@ -13,18 +16,36 @@ function printRouteTable(routes) {
|
|
|
13
16
|
}
|
|
14
17
|
console.log('');
|
|
15
18
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// Parse args: entry and --port or positional port
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const portFlag = args.indexOf('--port');
|
|
22
|
+
let port = 3000;
|
|
23
|
+
let entry = '';
|
|
24
|
+
if (portFlag !== -1) {
|
|
25
|
+
port = Number(args[portFlag + 1]) || 3000;
|
|
26
|
+
entry = args.filter((_, i) => i !== portFlag && i !== portFlag + 1).find((a) => !a.startsWith('-')) ?? '';
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const nonFlags = args.filter((a) => !a.startsWith('-'));
|
|
30
|
+
entry = nonFlags[0] ?? '';
|
|
31
|
+
port = nonFlags[1] ? Number(nonFlags[1]) || 3000 : 3000;
|
|
32
|
+
}
|
|
33
|
+
// Load config
|
|
34
|
+
const config = loadConfig() ?? {};
|
|
35
|
+
// Auto-detect entry: CLI arg > config > src/app.ts
|
|
36
|
+
const entryFile = entry || config.entry || 'src/app.ts';
|
|
37
|
+
const entryPath = resolve(entryFile);
|
|
38
|
+
if (!existsSync(entryPath)) {
|
|
39
|
+
console.error(`Entry file not found: ${entryPath}`);
|
|
40
|
+
console.error('Create src/app.ts or specify entry with: tzin dev <entry>');
|
|
20
41
|
process.exit(1);
|
|
21
42
|
}
|
|
22
|
-
const mod = await import(pathToFileURL(
|
|
43
|
+
const mod = await import(pathToFileURL(entryPath).href);
|
|
23
44
|
const app = mod.default ?? mod.app;
|
|
24
45
|
if (!app || typeof app.fetch !== 'function' || !Array.isArray(app.routes)) {
|
|
25
|
-
console.error('
|
|
46
|
+
console.error('Entry file must export a tzin App (createApp(...))');
|
|
26
47
|
process.exit(1);
|
|
27
48
|
}
|
|
28
49
|
printRouteTable(app.routes);
|
|
29
|
-
listen(app, port);
|
|
30
|
-
console.log(`\x1b[32m➜\x1b[0m http://localhost:${port} (watching for changes)`);
|
|
50
|
+
listen(app, config.port ?? port);
|
|
51
|
+
console.log(`\x1b[32m➜\x1b[0m http://localhost:${config.port ?? port} (watching for changes)`);
|
package/dist/index.d.ts
CHANGED
|
@@ -20,3 +20,5 @@ export { wsChannels, type WsRoute, type WsSend, type WsChannelOptions } from './
|
|
|
20
20
|
export { attachChannels } from './ws-node.js';
|
|
21
21
|
export { LocalBus, clusterHubs, type MessageBus } from './bus.js';
|
|
22
22
|
export { cors, type CorsOptions } from './cors.js';
|
|
23
|
+
export { defineConfig, loadConfig, type TzinConfig } from './config.js';
|
|
24
|
+
export { loadRoutes, type RouteLoaderOptions } from './route-loader.js';
|
package/dist/index.js
CHANGED
|
@@ -20,3 +20,5 @@ export { wsChannels } from './ws.js';
|
|
|
20
20
|
export { attachChannels } from './ws-node.js';
|
|
21
21
|
export { LocalBus, clusterHubs } from './bus.js';
|
|
22
22
|
export { cors } from './cors.js';
|
|
23
|
+
export { defineConfig, loadConfig } from './config.js';
|
|
24
|
+
export { loadRoutes } from './route-loader.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RouteImpl } from './contract.js';
|
|
2
|
+
export interface RouteLoaderOptions {
|
|
3
|
+
/** Directory to scan for routes */
|
|
4
|
+
dir?: string;
|
|
5
|
+
/** Glob patterns to include */
|
|
6
|
+
include?: string[];
|
|
7
|
+
/** Glob patterns to exclude */
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Auto-discover and load routes from a directory structure.
|
|
12
|
+
*
|
|
13
|
+
* Convention:
|
|
14
|
+
* - Each file exports route implementations
|
|
15
|
+
* - File path becomes the route prefix
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadRoutes(options?: RouteLoaderOptions): Promise<RouteImpl[]>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Auto-discover and load routes from a directory structure.
|
|
5
|
+
*
|
|
6
|
+
* Convention:
|
|
7
|
+
* - Each file exports route implementations
|
|
8
|
+
* - File path becomes the route prefix
|
|
9
|
+
*/
|
|
10
|
+
export async function loadRoutes(options = {}) {
|
|
11
|
+
const dir = resolve(options.dir || 'src/routes');
|
|
12
|
+
const routes = [];
|
|
13
|
+
try {
|
|
14
|
+
await scanDir(dir, '', routes);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Directory doesn't exist or is empty
|
|
18
|
+
}
|
|
19
|
+
return routes;
|
|
20
|
+
}
|
|
21
|
+
async function scanDir(dir, prefix, routes) {
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = join(dir, entry.name);
|
|
25
|
+
const routePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
await scanDir(fullPath, routePath, routes);
|
|
28
|
+
}
|
|
29
|
+
else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
|
|
30
|
+
// Skip index files and files starting with _
|
|
31
|
+
if (entry.name === 'index.ts' || entry.name === 'index.js')
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.name.startsWith('_'))
|
|
34
|
+
continue;
|
|
35
|
+
const module = await import(fullPath);
|
|
36
|
+
const exports = Object.values(module);
|
|
37
|
+
for (const exp of exports) {
|
|
38
|
+
if (isRouteImpl(exp)) {
|
|
39
|
+
routes.push(exp);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isRouteImpl(value) {
|
|
46
|
+
return (typeof value === 'object' &&
|
|
47
|
+
value !== null &&
|
|
48
|
+
'contract' in value &&
|
|
49
|
+
'handler' in value);
|
|
50
|
+
}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { AnyContract, SectionsOf, ResponseOf } from './contract.js';
|
|
2
|
+
import type { App } from './server.js';
|
|
3
|
+
export interface TestClient {
|
|
4
|
+
/** Make a GET request */
|
|
5
|
+
get(path: string, init?: RequestInit): Promise<TestResponse>;
|
|
6
|
+
/** Make a POST request */
|
|
7
|
+
post(path: string, body?: unknown, init?: RequestInit): Promise<TestResponse>;
|
|
8
|
+
/** Make a PUT request */
|
|
9
|
+
put(path: string, body?: unknown, init?: RequestInit): Promise<TestResponse>;
|
|
10
|
+
/** Make a PATCH request */
|
|
11
|
+
patch(path: string, body?: unknown, init?: RequestInit): Promise<TestResponse>;
|
|
12
|
+
/** Make a DELETE request */
|
|
13
|
+
delete(path: string, init?: RequestInit): Promise<TestResponse>;
|
|
14
|
+
/** Make a custom request */
|
|
15
|
+
request(path: string, init: RequestInit): Promise<TestResponse>;
|
|
16
|
+
/** Close the server */
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
/** Get the base URL */
|
|
19
|
+
readonly baseUrl: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TestResponse {
|
|
22
|
+
status: number;
|
|
23
|
+
headers: Headers;
|
|
24
|
+
body: unknown;
|
|
25
|
+
json<T = unknown>(): Promise<T>;
|
|
26
|
+
text(): Promise<string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a test client for an app.
|
|
30
|
+
* Starts a server, makes requests, and cleans up.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const api = await createTestClient(app)
|
|
35
|
+
* const res = await api.get('/users/1')
|
|
36
|
+
* expect(res.status).toBe(200)
|
|
37
|
+
* expect(res.body).toEqual({ id: '1', name: 'Ada' })
|
|
38
|
+
* await api.close()
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createTestClient(app: App): Promise<TestClient>;
|
|
42
|
+
/**
|
|
43
|
+
* Test that a handler satisfies its contract.
|
|
44
|
+
* Calls the handler and validates the response against the declared schemas.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* testContract(getUser, async (call) => {
|
|
49
|
+
* const res = await call({ params: { id: '1' } })
|
|
50
|
+
* expect(res.status).toBe(200)
|
|
51
|
+
* expect(res.body).toMatchSchema(getUser.responses[200])
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function testContract<C extends AnyContract>(contract: C, fn: (call: (input: SectionsOf<C>) => Promise<ResponseOf<C>>) => Promise<void>): void;
|
|
56
|
+
/**
|
|
57
|
+
* Validate that a value matches a schema.
|
|
58
|
+
* Returns true if valid, throws with details if invalid.
|
|
59
|
+
*/
|
|
60
|
+
export declare function expectSchema(schema: unknown, value: unknown): void;
|
|
61
|
+
/**
|
|
62
|
+
* Create a typed mock for a contract's sections.
|
|
63
|
+
* Useful for generating test data.
|
|
64
|
+
*/
|
|
65
|
+
export declare function mockSections<C extends AnyContract>(contract: C, overrides?: Partial<SectionsOf<C>>): SectionsOf<C>;
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Value } from './schema.js';
|
|
2
|
+
import { listen } from './node.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a test client for an app.
|
|
5
|
+
* Starts a server, makes requests, and cleans up.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const api = await createTestClient(app)
|
|
10
|
+
* const res = await api.get('/users/1')
|
|
11
|
+
* expect(res.status).toBe(200)
|
|
12
|
+
* expect(res.body).toEqual({ id: '1', name: 'Ada' })
|
|
13
|
+
* await api.close()
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export async function createTestClient(app) {
|
|
17
|
+
const server = await listen(app, 0);
|
|
18
|
+
const port = server.address().port;
|
|
19
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
20
|
+
async function request(path, init = {}) {
|
|
21
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
22
|
+
...init,
|
|
23
|
+
headers: {
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
...init.headers,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
let body = null;
|
|
29
|
+
try {
|
|
30
|
+
body = await res.json();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
try {
|
|
34
|
+
body = await res.text();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
body = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
status: res.status,
|
|
42
|
+
headers: res.headers,
|
|
43
|
+
body,
|
|
44
|
+
json: () => Promise.resolve(body),
|
|
45
|
+
text: () => Promise.resolve(typeof body === 'string' ? body : JSON.stringify(body)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
baseUrl,
|
|
50
|
+
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
51
|
+
post: (path, body, init) => request(path, { ...init, method: 'POST', body: body != null ? JSON.stringify(body) : undefined }),
|
|
52
|
+
put: (path, body, init) => request(path, { ...init, method: 'PUT', body: body != null ? JSON.stringify(body) : undefined }),
|
|
53
|
+
patch: (path, body, init) => request(path, { ...init, method: 'PATCH', body: body != null ? JSON.stringify(body) : undefined }),
|
|
54
|
+
delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
|
|
55
|
+
request,
|
|
56
|
+
close: () => new Promise((resolve, reject) => {
|
|
57
|
+
server.closeAllConnections?.();
|
|
58
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Test that a handler satisfies its contract.
|
|
64
|
+
* Calls the handler and validates the response against the declared schemas.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* testContract(getUser, async (call) => {
|
|
69
|
+
* const res = await call({ params: { id: '1' } })
|
|
70
|
+
* expect(res.status).toBe(200)
|
|
71
|
+
* expect(res.body).toMatchSchema(getUser.responses[200])
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function testContract(contract, fn) {
|
|
76
|
+
// This is a higher-order test helper - actual test framework integration
|
|
77
|
+
// would require a adapter for vitest/jest/etc.
|
|
78
|
+
// For now, export the utility and let users wrap it.
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate that a value matches a schema.
|
|
82
|
+
* Returns true if valid, throws with details if invalid.
|
|
83
|
+
*/
|
|
84
|
+
export function expectSchema(schema, value) {
|
|
85
|
+
if (!Value.Check(schema, value)) {
|
|
86
|
+
const errors = [...Value.Errors(schema, value)];
|
|
87
|
+
const details = errors.map((e) => `${e.path || '/'}: ${e.message}`).join('\n');
|
|
88
|
+
throw new Error(`Schema validation failed:\n${details}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create a typed mock for a contract's sections.
|
|
93
|
+
* Useful for generating test data.
|
|
94
|
+
*/
|
|
95
|
+
export function mockSections(contract, overrides) {
|
|
96
|
+
const sections = {};
|
|
97
|
+
if ('params' in contract && contract.params) {
|
|
98
|
+
sections.params = generateMock(contract.params);
|
|
99
|
+
}
|
|
100
|
+
if ('query' in contract && contract.query) {
|
|
101
|
+
sections.query = generateMock(contract.query);
|
|
102
|
+
}
|
|
103
|
+
if ('body' in contract && contract.body) {
|
|
104
|
+
sections.body = generateMock(contract.body);
|
|
105
|
+
}
|
|
106
|
+
return { ...sections, ...overrides };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Generate mock data from a TypeBox schema.
|
|
110
|
+
*/
|
|
111
|
+
function generateMock(schema) {
|
|
112
|
+
const s = schema;
|
|
113
|
+
if (s.type === 'string')
|
|
114
|
+
return 'mock-string';
|
|
115
|
+
if (s.type === 'number')
|
|
116
|
+
return 42;
|
|
117
|
+
if (s.type === 'boolean')
|
|
118
|
+
return true;
|
|
119
|
+
if (s.type === 'array') {
|
|
120
|
+
const items = s.items ? generateMock(s.items) : 'mock-item';
|
|
121
|
+
return [items];
|
|
122
|
+
}
|
|
123
|
+
if (s.type === 'object' && s.properties) {
|
|
124
|
+
const obj = {};
|
|
125
|
+
for (const [key, value] of Object.entries(s.properties)) {
|
|
126
|
+
obj[key] = generateMock(value);
|
|
127
|
+
}
|
|
128
|
+
return obj;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlos-tzin/tzin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Contract-first TypeScript framework. Types that scale, realtime channels with presence, and an MCP server for every API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "The tzin authors",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/Charly921/tzin"
|
|
24
|
+
"url": "git+https://github.com/Charly921/tzin.git"
|
|
25
25
|
},
|
|
26
26
|
"type": "module",
|
|
27
27
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"./ws": {
|
|
43
43
|
"types": "./dist/ws.d.ts",
|
|
44
44
|
"default": "./dist/ws.js"
|
|
45
|
+
},
|
|
46
|
+
"./test": {
|
|
47
|
+
"types": "./dist/test.d.ts",
|
|
48
|
+
"default": "./dist/test.js"
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
"files": [
|