typed-bridge 2.1.5 → 3.0.1
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/dist/bridge/index.d.ts +9 -4
- package/dist/bridge/index.js +32 -4
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +2 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.js +119 -0
- package/dist/scripts/buildTypeBridge.js +34 -1
- package/dist/scripts/typedBridgeCleaner.js +247 -28
- package/dist/tools/index.d.ts +126 -0
- package/dist/tools/index.js +218 -0
- package/package.json +18 -3
- package/readme.md +203 -234
- package/.vscode/settings.json +0 -3
- package/dist/demo/bridge/index.d.ts +0 -255
- package/dist/demo/bridge/index.js +0 -56
- package/dist/demo/bridge/order/index.d.ts +0 -42
- package/dist/demo/bridge/order/index.js +0 -165
- package/dist/demo/bridge/order/types.d.ts +0 -187
- package/dist/demo/bridge/order/types.js +0 -150
- package/dist/demo/bridge/product/index.d.ts +0 -16
- package/dist/demo/bridge/product/index.js +0 -67
- package/dist/demo/bridge/product/types.d.ts +0 -24
- package/dist/demo/bridge/product/types.js +0 -27
- package/dist/demo/bridge/user/index.d.ts +0 -28
- package/dist/demo/bridge/user/index.js +0 -86
- package/dist/demo/bridge/user/types.d.ts +0 -23
- package/dist/demo/bridge/user/types.js +0 -26
- package/dist/demo/index.d.ts +0 -1
- package/dist/demo/index.js +0 -23
- package/dist/demo/middleware.d.ts +0 -1
- package/dist/demo/middleware.js +0 -58
- package/test/bridge.ts +0 -283
- package/test/index.ts +0 -251
package/dist/bridge/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Application, Request, Response } from 'express';
|
|
2
2
|
import { Server } from 'http';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { MCPGetContext } from '../mcp';
|
|
4
|
+
import { Bridge, BridgeEntries, LLMToolFormat } from '../tools';
|
|
5
|
+
interface CreateBridgeOptions {
|
|
6
|
+
entries?: BridgeEntries;
|
|
7
|
+
toolsFormat?: LLMToolFormat;
|
|
8
|
+
mcp?: boolean | string;
|
|
9
|
+
mcpGetContext?: MCPGetContext;
|
|
10
|
+
}
|
|
6
11
|
type Middleware = {
|
|
7
12
|
pattern: string;
|
|
8
13
|
handler: (req: Request, res: Response) => Promise<{
|
|
@@ -12,7 +17,7 @@ type Middleware = {
|
|
|
12
17
|
};
|
|
13
18
|
export declare const createMiddleware: (pattern: string, handler: Middleware["handler"]) => number;
|
|
14
19
|
export declare const onShutdown: (fn: () => void) => () => void;
|
|
15
|
-
export declare const createBridge: (bridge: Bridge, port: number, path?: string) => {
|
|
20
|
+
export declare const createBridge: (bridge: Bridge, port: number, path?: string, options?: CreateBridgeOptions) => {
|
|
16
21
|
app: Application;
|
|
17
22
|
server: Server;
|
|
18
23
|
};
|
package/dist/bridge/index.js
CHANGED
|
@@ -11,13 +11,15 @@ const express_1 = __importDefault(require("express"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const __1 = require("..");
|
|
13
13
|
const helpers_1 = require("../helpers");
|
|
14
|
+
const mcp_1 = require("../mcp");
|
|
15
|
+
const tools_1 = require("../tools");
|
|
14
16
|
const middlewares = [];
|
|
15
17
|
const createMiddleware = (pattern, handler) => middlewares.push({ pattern, handler });
|
|
16
18
|
exports.createMiddleware = createMiddleware;
|
|
17
19
|
let shutdownCallback = () => { };
|
|
18
20
|
const onShutdown = (fn) => (shutdownCallback = fn);
|
|
19
21
|
exports.onShutdown = onShutdown;
|
|
20
|
-
const createBridge = (bridge, port, path = '/bridge') => {
|
|
22
|
+
const createBridge = (bridge, port, path = '/bridge', options) => {
|
|
21
23
|
const app = (0, express_1.default)();
|
|
22
24
|
// cors
|
|
23
25
|
app.use((0, cors_1.default)());
|
|
@@ -84,6 +86,32 @@ const createBridge = (bridge, port, path = '/bridge') => {
|
|
|
84
86
|
timestamp: new Date().toISOString()
|
|
85
87
|
});
|
|
86
88
|
});
|
|
89
|
+
// LLM tools endpoint
|
|
90
|
+
if (options?.entries) {
|
|
91
|
+
const entries = options.entries;
|
|
92
|
+
const defaultFormat = options.toolsFormat || 'openai';
|
|
93
|
+
app.get(path_1.default.join(path, 'tools'), (req, res) => {
|
|
94
|
+
const requestedFormat = req.query.format;
|
|
95
|
+
// No format provided → use the configured default
|
|
96
|
+
if (requestedFormat === undefined) {
|
|
97
|
+
res.json((0, tools_1.toLLMTools)(entries, { format: defaultFormat }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Reject unknown formats instead of silently returning an empty tool list
|
|
101
|
+
if (typeof requestedFormat !== 'string' || !(0, tools_1.isLLMToolFormat)(requestedFormat)) {
|
|
102
|
+
res.status(400).json({
|
|
103
|
+
error: `Invalid format: ${String(requestedFormat)}. Expected one of ${tools_1.LLM_TOOL_FORMATS.join(', ')}`
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
res.json((0, tools_1.toLLMTools)(entries, { format: requestedFormat }));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// MCP endpoint
|
|
111
|
+
if (options?.mcp && options?.entries) {
|
|
112
|
+
const mcpPath = typeof options.mcp === 'string' ? options.mcp : path_1.default.join(path, 'mcp');
|
|
113
|
+
(0, mcp_1.mountMCP)(app, bridge, options.entries, mcpPath, options.mcpGetContext);
|
|
114
|
+
}
|
|
87
115
|
app.use(path, bridgeHandler(bridge));
|
|
88
116
|
const server = app.listen(port, () => (0, helpers_1.printStartLogs)(port));
|
|
89
117
|
let shuttingDown = false;
|
|
@@ -136,9 +164,9 @@ const bridgeHandler = (bridge) => async (req, res) => {
|
|
|
136
164
|
console.error(`ARGS | ${id} ::`, JSON.stringify(args, null, 2));
|
|
137
165
|
if (__1.tbConfig.logs.contextOnError)
|
|
138
166
|
console.error(`CONTEXT | ${id} ::`, JSON.stringify(context, null, 2));
|
|
139
|
-
if (Array.isArray(error.
|
|
140
|
-
const keyPath = error.
|
|
141
|
-
const errorMessage = (keyPath ? keyPath + ': ' : '') + error.
|
|
167
|
+
if (Array.isArray(error.issues) && error.issues.length) {
|
|
168
|
+
const keyPath = error.issues[0].path.join('/');
|
|
169
|
+
const errorMessage = (keyPath ? keyPath + ': ' : '') + error.issues[0].message;
|
|
142
170
|
return res.status(400).send(errorMessage);
|
|
143
171
|
}
|
|
144
172
|
if (__1.tbConfig.logs.error)
|
package/dist/config/index.d.ts
CHANGED
package/dist/config/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { Application, default as express, Express, NextFunction, Request, Response, Router } from 'express';
|
|
2
2
|
export { createBridge, createMiddleware, onShutdown } from './bridge';
|
|
3
3
|
export { config as tbConfig } from './config';
|
|
4
|
+
export { mountMCP } from './mcp';
|
|
5
|
+
export type { MCPGetContext } from './mcp';
|
|
6
|
+
export { defineBridge, getMetaTools, handleMetaToolCall, toLLMTools, toolDescribe, toolSearch, toolUse } from './tools';
|
|
7
|
+
export type { Bridge, BridgeEntries, BridgeEntry, LLMToolFormat, ToLLMToolsOptions, ToolCall } from './tools';
|
|
4
8
|
export { z } from 'zod';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.z = exports.tbConfig = exports.onShutdown = exports.createMiddleware = exports.createBridge = exports.Router = exports.express = void 0;
|
|
6
|
+
exports.z = exports.toolUse = exports.toolSearch = exports.toolDescribe = exports.toLLMTools = exports.handleMetaToolCall = exports.getMetaTools = exports.defineBridge = exports.mountMCP = exports.tbConfig = exports.onShutdown = exports.createMiddleware = exports.createBridge = exports.Router = exports.express = void 0;
|
|
7
7
|
var express_1 = require("express");
|
|
8
8
|
Object.defineProperty(exports, "express", { enumerable: true, get: function () { return __importDefault(express_1).default; } });
|
|
9
9
|
Object.defineProperty(exports, "Router", { enumerable: true, get: function () { return express_1.Router; } });
|
|
@@ -13,5 +13,15 @@ Object.defineProperty(exports, "createMiddleware", { enumerable: true, get: func
|
|
|
13
13
|
Object.defineProperty(exports, "onShutdown", { enumerable: true, get: function () { return bridge_1.onShutdown; } });
|
|
14
14
|
var config_1 = require("./config");
|
|
15
15
|
Object.defineProperty(exports, "tbConfig", { enumerable: true, get: function () { return config_1.config; } });
|
|
16
|
+
var mcp_1 = require("./mcp");
|
|
17
|
+
Object.defineProperty(exports, "mountMCP", { enumerable: true, get: function () { return mcp_1.mountMCP; } });
|
|
18
|
+
var tools_1 = require("./tools");
|
|
19
|
+
Object.defineProperty(exports, "defineBridge", { enumerable: true, get: function () { return tools_1.defineBridge; } });
|
|
20
|
+
Object.defineProperty(exports, "getMetaTools", { enumerable: true, get: function () { return tools_1.getMetaTools; } });
|
|
21
|
+
Object.defineProperty(exports, "handleMetaToolCall", { enumerable: true, get: function () { return tools_1.handleMetaToolCall; } });
|
|
22
|
+
Object.defineProperty(exports, "toLLMTools", { enumerable: true, get: function () { return tools_1.toLLMTools; } });
|
|
23
|
+
Object.defineProperty(exports, "toolDescribe", { enumerable: true, get: function () { return tools_1.toolDescribe; } });
|
|
24
|
+
Object.defineProperty(exports, "toolSearch", { enumerable: true, get: function () { return tools_1.toolSearch; } });
|
|
25
|
+
Object.defineProperty(exports, "toolUse", { enumerable: true, get: function () { return tools_1.toolUse; } });
|
|
16
26
|
var zod_1 = require("zod");
|
|
17
27
|
Object.defineProperty(exports, "z", { enumerable: true, get: function () { return zod_1.z; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import { Application } from 'express';
|
|
3
|
+
import { Bridge, BridgeEntries } from '../tools';
|
|
4
|
+
export type MCPGetContext = (headers: IncomingHttpHeaders) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
5
|
+
export declare function mountMCP(app: Application, bridge: Bridge, entries: BridgeEntries, path?: string, getContext?: MCPGetContext): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mountMCP = mountMCP;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
6
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
7
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
8
|
+
const tools_1 = require("../tools");
|
|
9
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
10
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
11
|
+
const MAX_SESSIONS = 1000;
|
|
12
|
+
function createMCPServer(bridge, entries, headersRef, getContext) {
|
|
13
|
+
const server = new index_js_1.Server({ name: 'typed-bridge', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
14
|
+
// An entry is exposed to MCP unless it explicitly opts out with `mcp: false`
|
|
15
|
+
const isExposed = (entry) => !!entry && entry.mcp !== false;
|
|
16
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
17
|
+
const tools = Object.entries(entries)
|
|
18
|
+
.filter(([, entry]) => isExposed(entry))
|
|
19
|
+
.map(([name, entry]) => ({
|
|
20
|
+
name,
|
|
21
|
+
description: entry.description,
|
|
22
|
+
inputSchema: (0, tools_1.toToolInputSchema)(entry.args)
|
|
23
|
+
}));
|
|
24
|
+
return { tools };
|
|
25
|
+
});
|
|
26
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
27
|
+
const { name, arguments: args } = request.params;
|
|
28
|
+
try {
|
|
29
|
+
// Block execution of hidden tools, not just their listing
|
|
30
|
+
if (!isExposed(entries[name]))
|
|
31
|
+
throw new Error(`Tool not found: ${name}`);
|
|
32
|
+
const handler = bridge[name];
|
|
33
|
+
if (!handler)
|
|
34
|
+
throw new Error(`Tool not found: ${name}`);
|
|
35
|
+
// Derive per-request context from forwarded headers
|
|
36
|
+
const context = getContext ? await getContext(headersRef.current) : undefined;
|
|
37
|
+
const result = await handler(args || {}, context);
|
|
38
|
+
// Throws if the result exceeds tbConfig.maxToolOutputChars — handled below as isError
|
|
39
|
+
const text = (0, tools_1.enforceToolOutputLimit)(result);
|
|
40
|
+
return { content: [{ type: 'text', text }] };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return server;
|
|
48
|
+
}
|
|
49
|
+
function mountMCP(app, bridge, entries, path = '/mcp', getContext) {
|
|
50
|
+
const sessions = new Map();
|
|
51
|
+
// Evict sessions idle for longer than SESSION_TTL_MS
|
|
52
|
+
const sweepInterval = setInterval(() => {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const [id, session] of sessions) {
|
|
55
|
+
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
56
|
+
session.server.close().catch(() => { });
|
|
57
|
+
sessions.delete(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, SWEEP_INTERVAL_MS);
|
|
61
|
+
sweepInterval.unref();
|
|
62
|
+
const handler = async (req, res) => {
|
|
63
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
64
|
+
let transport;
|
|
65
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
66
|
+
const session = sessions.get(sessionId);
|
|
67
|
+
session.lastActivity = Date.now();
|
|
68
|
+
session.headersRef.current = req.headers;
|
|
69
|
+
transport = session.transport;
|
|
70
|
+
}
|
|
71
|
+
else if (!sessionId && req.method === 'POST') {
|
|
72
|
+
const headersRef = { current: req.headers };
|
|
73
|
+
const server = createMCPServer(bridge, entries, headersRef, getContext);
|
|
74
|
+
transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
75
|
+
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
|
|
76
|
+
onsessioninitialized: (id) => {
|
|
77
|
+
// Bound memory: evict the least-recently-active session when at capacity
|
|
78
|
+
if (sessions.size >= MAX_SESSIONS)
|
|
79
|
+
evictOldestSession(sessions);
|
|
80
|
+
sessions.set(id, { transport, server, lastActivity: Date.now(), headersRef });
|
|
81
|
+
transport.onclose = () => sessions.delete(id);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
await server.connect(transport);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
res.status(400).json({ error: 'Invalid or missing session' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await transport.handleRequest(req, res, req.body);
|
|
91
|
+
};
|
|
92
|
+
app.post(path, handler);
|
|
93
|
+
app.get(path, handler);
|
|
94
|
+
app.delete(path, async (req, res) => {
|
|
95
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
96
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
97
|
+
res.status(400).json({ error: 'Invalid or missing session' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const session = sessions.get(sessionId);
|
|
101
|
+
sessions.delete(sessionId);
|
|
102
|
+
await session.server.close().catch(() => { });
|
|
103
|
+
res.status(200).end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function evictOldestSession(sessions) {
|
|
107
|
+
let oldestId;
|
|
108
|
+
let oldestSession;
|
|
109
|
+
for (const [id, session] of sessions) {
|
|
110
|
+
if (!oldestSession || session.lastActivity < oldestSession.lastActivity) {
|
|
111
|
+
oldestId = id;
|
|
112
|
+
oldestSession = session;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (oldestId && oldestSession) {
|
|
116
|
+
oldestSession.server.close().catch(() => { });
|
|
117
|
+
sessions.delete(oldestId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -4,12 +4,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.default = build;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
7
8
|
const rollup_1 = require("rollup");
|
|
8
9
|
const rollup_plugin_dts_1 = __importDefault(require("rollup-plugin-dts"));
|
|
10
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
11
|
+
/**
|
|
12
|
+
* Consumers import `z` from `typed-bridge`, which re-exports typed-bridge's own
|
|
13
|
+
* nested zod (`typed-bridge/node_modules/zod`). When the consumer's bridge has no
|
|
14
|
+
* type annotation, declaration emit must name that nested zod and fails with
|
|
15
|
+
* TS2742 ("cannot be named without a reference to ... This is likely not portable").
|
|
16
|
+
*
|
|
17
|
+
* Aliasing a bare `zod` specifier to typed-bridge's zod makes the inferred types
|
|
18
|
+
* portably nameable, so emit succeeds. The cleaner strips every zod reference
|
|
19
|
+
* afterwards, so this only needs to satisfy the compiler, not the final output.
|
|
20
|
+
*/
|
|
21
|
+
function resolveTypeRoots(src) {
|
|
22
|
+
// Preserve the consumer's own path aliases (e.g. `@/*`) so handler files resolve
|
|
23
|
+
const configPath = typescript_1.default.findConfigFile(path_1.default.dirname(path_1.default.resolve(src)), typescript_1.default.sys.fileExists, 'tsconfig.json');
|
|
24
|
+
let baseUrl = process.cwd();
|
|
25
|
+
let paths = {};
|
|
26
|
+
if (configPath) {
|
|
27
|
+
const read = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
|
|
28
|
+
const parsed = typescript_1.default.parseJsonConfigFileContent(read.config, typescript_1.default.sys, path_1.default.dirname(configPath));
|
|
29
|
+
baseUrl = parsed.options.baseUrl || path_1.default.dirname(configPath);
|
|
30
|
+
paths = { ...(parsed.options.paths || {}) };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const zodDir = path_1.default.dirname(require.resolve('zod/package.json'));
|
|
34
|
+
paths = { ...paths, zod: [zodDir], 'zod/*': [path_1.default.join(zodDir, '*')] };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// zod not resolvable — fall back to default behaviour
|
|
38
|
+
}
|
|
39
|
+
return { baseUrl, paths };
|
|
40
|
+
}
|
|
9
41
|
async function build(src = '', dest = '') {
|
|
42
|
+
const { baseUrl, paths } = resolveTypeRoots(src);
|
|
10
43
|
const bundle = await (0, rollup_1.rollup)({
|
|
11
44
|
input: src,
|
|
12
|
-
plugins: [(0, rollup_plugin_dts_1.default)({ respectExternal: false })],
|
|
45
|
+
plugins: [(0, rollup_plugin_dts_1.default)({ respectExternal: false, compilerOptions: { baseUrl, paths } })],
|
|
13
46
|
onwarn(warning, warn) {
|
|
14
47
|
if (warning.code === 'UNRESOLVED_IMPORT')
|
|
15
48
|
return;
|
|
@@ -9,31 +9,28 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const typescript_1 = __importDefault(require("typescript"));
|
|
10
10
|
// Snippet to inject at the end
|
|
11
11
|
const proxySnippet = () => `
|
|
12
|
-
type
|
|
12
|
+
type TypedBridgeConfig = {
|
|
13
13
|
host: string
|
|
14
|
-
headers:
|
|
15
|
-
onResponse: (
|
|
14
|
+
headers: Record<string, string>
|
|
15
|
+
onResponse: (response: Response) => void
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const typedBridgeConfig:
|
|
18
|
+
export const typedBridgeConfig: TypedBridgeConfig = {
|
|
19
19
|
host: '',
|
|
20
20
|
headers: { 'Content-Type': 'application/json' },
|
|
21
|
-
onResponse: (
|
|
21
|
+
onResponse: () => {}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const typedBridge = new Proxy(
|
|
25
25
|
{},
|
|
26
26
|
{
|
|
27
27
|
get(_, methodName: string) {
|
|
28
|
-
return async (args:
|
|
28
|
+
return async (args: unknown) => {
|
|
29
29
|
const response = await fetch(
|
|
30
30
|
typedBridgeConfig.host + (typedBridgeConfig.host.endsWith('/') ? '' : '/') + methodName,
|
|
31
31
|
{
|
|
32
32
|
method: 'POST',
|
|
33
|
-
headers:
|
|
34
|
-
'Content-Type': 'application/json',
|
|
35
|
-
...typedBridgeConfig.headers
|
|
36
|
-
},
|
|
33
|
+
headers: typedBridgeConfig.headers,
|
|
37
34
|
body: JSON.stringify(args)
|
|
38
35
|
}
|
|
39
36
|
)
|
|
@@ -53,7 +50,7 @@ export const typedBridge = new Proxy(
|
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
|
-
) as
|
|
53
|
+
) as TypedBridge
|
|
57
54
|
|
|
58
55
|
export default typedBridge
|
|
59
56
|
`;
|
|
@@ -72,7 +69,27 @@ const resolveZodTypesTransformer = context => {
|
|
|
72
69
|
ZodVoid: typescript_1.default.SyntaxKind.VoidKeyword,
|
|
73
70
|
ZodAny: typescript_1.default.SyntaxKind.AnyKeyword,
|
|
74
71
|
ZodUnknown: typescript_1.default.SyntaxKind.UnknownKeyword,
|
|
75
|
-
ZodNever: typescript_1.default.SyntaxKind.NeverKeyword
|
|
72
|
+
ZodNever: typescript_1.default.SyntaxKind.NeverKeyword,
|
|
73
|
+
// Zod 4 branded string types
|
|
74
|
+
ZodEmail: typescript_1.default.SyntaxKind.StringKeyword,
|
|
75
|
+
ZodURL: typescript_1.default.SyntaxKind.StringKeyword,
|
|
76
|
+
ZodUUID: typescript_1.default.SyntaxKind.StringKeyword,
|
|
77
|
+
ZodEmoji: typescript_1.default.SyntaxKind.StringKeyword,
|
|
78
|
+
ZodNanoID: typescript_1.default.SyntaxKind.StringKeyword,
|
|
79
|
+
ZodCUID: typescript_1.default.SyntaxKind.StringKeyword,
|
|
80
|
+
ZodCUID2: typescript_1.default.SyntaxKind.StringKeyword,
|
|
81
|
+
ZodULID: typescript_1.default.SyntaxKind.StringKeyword,
|
|
82
|
+
ZodIPv4: typescript_1.default.SyntaxKind.StringKeyword,
|
|
83
|
+
ZodIPv6: typescript_1.default.SyntaxKind.StringKeyword,
|
|
84
|
+
ZodCIDRv4: typescript_1.default.SyntaxKind.StringKeyword,
|
|
85
|
+
ZodCIDRv6: typescript_1.default.SyntaxKind.StringKeyword,
|
|
86
|
+
ZodBase64: typescript_1.default.SyntaxKind.StringKeyword,
|
|
87
|
+
ZodBase64URL: typescript_1.default.SyntaxKind.StringKeyword,
|
|
88
|
+
ZodJWT: typescript_1.default.SyntaxKind.StringKeyword,
|
|
89
|
+
// Zod 4 branded number types
|
|
90
|
+
ZodInt: typescript_1.default.SyntaxKind.NumberKeyword,
|
|
91
|
+
ZodFloat32: typescript_1.default.SyntaxKind.NumberKeyword,
|
|
92
|
+
ZodFloat64: typescript_1.default.SyntaxKind.NumberKeyword
|
|
76
93
|
};
|
|
77
94
|
function getZodRef(node) {
|
|
78
95
|
if (typescript_1.default.isTypeReferenceNode(node) &&
|
|
@@ -206,31 +223,230 @@ const removeSecondParamTransformer = context => {
|
|
|
206
223
|
};
|
|
207
224
|
/**
|
|
208
225
|
* Transformer #3:
|
|
209
|
-
*
|
|
226
|
+
* Make any parameter typed as `undefined` optional so callers can omit it.
|
|
227
|
+
* e.g. `(_args: undefined) => Promise<...>` → `(_args?: undefined) => Promise<...>`
|
|
210
228
|
*/
|
|
211
|
-
const
|
|
229
|
+
const optionalUndefinedParamTransformer = context => {
|
|
212
230
|
return sourceFile => {
|
|
213
231
|
function visitor(node) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
if (typescript_1.default.isFunctionTypeNode(node)) {
|
|
233
|
+
const params = node.parameters.map(param => {
|
|
234
|
+
if (param.type &&
|
|
235
|
+
typescript_1.default.isToken(param.type) &&
|
|
236
|
+
param.type.kind === typescript_1.default.SyntaxKind.UndefinedKeyword &&
|
|
237
|
+
!param.questionToken) {
|
|
238
|
+
return typescript_1.default.factory.updateParameterDeclaration(param, param.modifiers, param.dotDotDotToken, param.name, typescript_1.default.factory.createToken(typescript_1.default.SyntaxKind.QuestionToken), param.type, param.initializer);
|
|
239
|
+
}
|
|
240
|
+
return param;
|
|
241
|
+
});
|
|
242
|
+
return typescript_1.default.factory.updateFunctionTypeNode(node, node.typeParameters, typescript_1.default.factory.createNodeArray(params), node.type);
|
|
222
243
|
}
|
|
223
244
|
return typescript_1.default.visitEachChild(node, visitor, context);
|
|
224
245
|
}
|
|
246
|
+
return typescript_1.default.visitEachChild(sourceFile, visitor, context);
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Transformer #4:
|
|
251
|
+
* Resolve ExtractHandlers<T> in the _default declaration by unwrapping
|
|
252
|
+
* { handler: fn, ... } entries to just the function type.
|
|
253
|
+
* Also removes helper type definitions and the entries declaration that
|
|
254
|
+
* leak from defineBridge() usage.
|
|
255
|
+
*/
|
|
256
|
+
const unwrapBridgeEntryTransformer = _context => {
|
|
257
|
+
return sourceFile => {
|
|
258
|
+
const helperTypes = new Set(['Bridge', 'BridgeEntry', 'BridgeEntries', 'ExtractHandlers']);
|
|
259
|
+
// Detect the variable bound to the default export. rollup-plugin-dts names an
|
|
260
|
+
// anonymous `export default defineBridge(...)` as `_default`, but keeps the
|
|
261
|
+
// user's identifier (e.g. `bridge`) when they do `const bridge = ...; export default bridge`.
|
|
262
|
+
let defaultName;
|
|
263
|
+
for (const stmt of sourceFile.statements) {
|
|
264
|
+
if (typescript_1.default.isExportAssignment(stmt) && !stmt.isExportEquals && typescript_1.default.isIdentifier(stmt.expression)) {
|
|
265
|
+
defaultName = stmt.expression.text;
|
|
266
|
+
}
|
|
267
|
+
if (typescript_1.default.isExportDeclaration(stmt) && stmt.exportClause && typescript_1.default.isNamedExports(stmt.exportClause)) {
|
|
268
|
+
for (const el of stmt.exportClause.elements) {
|
|
269
|
+
if (el.name.text === 'default')
|
|
270
|
+
defaultName = (el.propertyName ?? el.name).text;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function resolveHandlers(typeLiteral) {
|
|
275
|
+
const members = typeLiteral.members.map(member => {
|
|
276
|
+
if (!typescript_1.default.isPropertySignature(member) || !member.type)
|
|
277
|
+
return member;
|
|
278
|
+
if (typescript_1.default.isTypeLiteralNode(member.type)) {
|
|
279
|
+
const handlerProp = member.type.members.find(m => typescript_1.default.isPropertySignature(m) &&
|
|
280
|
+
typescript_1.default.isIdentifier(m.name) &&
|
|
281
|
+
m.name.text === 'handler' &&
|
|
282
|
+
m.type &&
|
|
283
|
+
typescript_1.default.isFunctionTypeNode(m.type));
|
|
284
|
+
if (handlerProp && typescript_1.default.isPropertySignature(handlerProp) && handlerProp.type) {
|
|
285
|
+
return typescript_1.default.factory.updatePropertySignature(member, member.modifiers, member.name, member.questionToken, handlerProp.type);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return member;
|
|
289
|
+
});
|
|
290
|
+
return typescript_1.default.factory.createTypeLiteralNode(members);
|
|
291
|
+
}
|
|
225
292
|
const updatedStatements = [];
|
|
226
293
|
for (const stmt of sourceFile.statements) {
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
294
|
+
// Remove helper type aliases
|
|
295
|
+
if (typescript_1.default.isTypeAliasDeclaration(stmt) && helperTypes.has(stmt.name.text))
|
|
296
|
+
continue;
|
|
297
|
+
// Remove entries declaration
|
|
298
|
+
if (typescript_1.default.isVariableStatement(stmt)) {
|
|
299
|
+
const decl = stmt.declarationList.declarations[0];
|
|
300
|
+
if (decl && typescript_1.default.isIdentifier(decl.name) && decl.name.text === 'entries')
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Convert the default-export variable to the TypedBridge type alias
|
|
304
|
+
if (typescript_1.default.isVariableStatement(stmt)) {
|
|
305
|
+
const decl = stmt.declarationList.declarations[0];
|
|
306
|
+
if (decl && typescript_1.default.isIdentifier(decl.name) && decl.name.text === defaultName && decl.type) {
|
|
307
|
+
let resolvedType;
|
|
308
|
+
// `ExtractHandlers` may be a bare identifier (inlined, same package) or a
|
|
309
|
+
// namespace-qualified reference (e.g. `typed_bridge_dist_tools.ExtractHandlers`)
|
|
310
|
+
// when typed-bridge is an external dependency in a consumer project.
|
|
311
|
+
const refName = typescript_1.default.isTypeReferenceNode(decl.type) &&
|
|
312
|
+
(typescript_1.default.isIdentifier(decl.type.typeName)
|
|
313
|
+
? decl.type.typeName.text
|
|
314
|
+
: decl.type.typeName.right.text);
|
|
315
|
+
if (typescript_1.default.isTypeReferenceNode(decl.type) &&
|
|
316
|
+
refName === 'ExtractHandlers' &&
|
|
317
|
+
decl.type.typeArguments?.length === 1 &&
|
|
318
|
+
typescript_1.default.isTypeLiteralNode(decl.type.typeArguments[0])) {
|
|
319
|
+
resolvedType = resolveHandlers(decl.type.typeArguments[0]);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
resolvedType = decl.type;
|
|
323
|
+
}
|
|
324
|
+
updatedStatements.push(typescript_1.default.factory.createTypeAliasDeclaration(undefined, 'TypedBridge', undefined, resolvedType));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Strip entries from named exports
|
|
329
|
+
if (typescript_1.default.isExportDeclaration(stmt) && stmt.exportClause && typescript_1.default.isNamedExports(stmt.exportClause)) {
|
|
330
|
+
const filtered = stmt.exportClause.elements.filter(el => el.name.text !== 'entries' && el.propertyName?.text !== 'entries');
|
|
331
|
+
if (filtered.length === 0)
|
|
332
|
+
continue;
|
|
333
|
+
if (filtered.length !== stmt.exportClause.elements.length) {
|
|
334
|
+
updatedStatements.push(typescript_1.default.factory.updateExportDeclaration(stmt, stmt.modifiers, stmt.isTypeOnly, typescript_1.default.factory.updateNamedExports(stmt.exportClause, filtered), stmt.moduleSpecifier, stmt.attributes));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
updatedStatements.push(stmt);
|
|
230
339
|
}
|
|
231
340
|
return typescript_1.default.factory.updateSourceFile(sourceFile, typescript_1.default.factory.createNodeArray(updatedStatements));
|
|
232
341
|
};
|
|
233
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Transformer #5:
|
|
345
|
+
* Remove the rollup default export (`export { X as default }` for any X, or
|
|
346
|
+
* `export default X`). The proxy snippet provides its own default export.
|
|
347
|
+
*/
|
|
348
|
+
const removeDefaultExportTransformer = _context => {
|
|
349
|
+
return sourceFile => {
|
|
350
|
+
const updatedStatements = [];
|
|
351
|
+
for (const stmt of sourceFile.statements) {
|
|
352
|
+
// Drop `export default X` / `export = X`
|
|
353
|
+
if (typescript_1.default.isExportAssignment(stmt))
|
|
354
|
+
continue;
|
|
355
|
+
// Filter `... as default` specifiers out of named exports
|
|
356
|
+
if (typescript_1.default.isExportDeclaration(stmt) && stmt.exportClause && typescript_1.default.isNamedExports(stmt.exportClause)) {
|
|
357
|
+
const filtered = stmt.exportClause.elements.filter(el => el.name.text !== 'default');
|
|
358
|
+
if (filtered.length === 0)
|
|
359
|
+
continue;
|
|
360
|
+
if (filtered.length !== stmt.exportClause.elements.length) {
|
|
361
|
+
updatedStatements.push(typescript_1.default.factory.updateExportDeclaration(stmt, stmt.modifiers, stmt.isTypeOnly, typescript_1.default.factory.updateNamedExports(stmt.exportClause, filtered), stmt.moduleSpecifier, stmt.attributes));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
updatedStatements.push(stmt);
|
|
366
|
+
}
|
|
367
|
+
return typescript_1.default.factory.updateSourceFile(sourceFile, typescript_1.default.factory.createNodeArray(updatedStatements));
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Transformer #6:
|
|
372
|
+
* Drop every top-level statement that is not reachable from the `TypedBridge`
|
|
373
|
+
* alias. When typed-bridge is an external dependency, rollup leaves behind stray
|
|
374
|
+
* imports (e.g. `import * as ... from 'typed-bridge/dist/tools'`) and inlined
|
|
375
|
+
* context types (e.g. `adminContext`) that the resolved client no longer needs.
|
|
376
|
+
*/
|
|
377
|
+
const pruneUnreachableTransformer = _context => {
|
|
378
|
+
return sourceFile => {
|
|
379
|
+
const statements = sourceFile.statements;
|
|
380
|
+
const typedBridge = statements.find((s) => typescript_1.default.isTypeAliasDeclaration(s) && s.name.text === 'TypedBridge');
|
|
381
|
+
if (!typedBridge)
|
|
382
|
+
return sourceFile;
|
|
383
|
+
const namesOf = (stmt) => {
|
|
384
|
+
if (typescript_1.default.isTypeAliasDeclaration(stmt) ||
|
|
385
|
+
typescript_1.default.isInterfaceDeclaration(stmt) ||
|
|
386
|
+
typescript_1.default.isClassDeclaration(stmt) ||
|
|
387
|
+
typescript_1.default.isEnumDeclaration(stmt) ||
|
|
388
|
+
typescript_1.default.isFunctionDeclaration(stmt)) {
|
|
389
|
+
return stmt.name ? [stmt.name.text] : [];
|
|
390
|
+
}
|
|
391
|
+
if (typescript_1.default.isVariableStatement(stmt)) {
|
|
392
|
+
return stmt.declarationList.declarations.flatMap(d => (typescript_1.default.isIdentifier(d.name) ? [d.name.text] : []));
|
|
393
|
+
}
|
|
394
|
+
if (typescript_1.default.isImportDeclaration(stmt) && stmt.importClause) {
|
|
395
|
+
const names = [];
|
|
396
|
+
const clause = stmt.importClause;
|
|
397
|
+
if (clause.name)
|
|
398
|
+
names.push(clause.name.text);
|
|
399
|
+
if (clause.namedBindings) {
|
|
400
|
+
if (typescript_1.default.isNamespaceImport(clause.namedBindings))
|
|
401
|
+
names.push(clause.namedBindings.name.text);
|
|
402
|
+
else
|
|
403
|
+
for (const el of clause.namedBindings.elements)
|
|
404
|
+
names.push(el.name.text);
|
|
405
|
+
}
|
|
406
|
+
return names;
|
|
407
|
+
}
|
|
408
|
+
return [];
|
|
409
|
+
};
|
|
410
|
+
const declarers = new Map();
|
|
411
|
+
for (const stmt of statements)
|
|
412
|
+
for (const n of namesOf(stmt))
|
|
413
|
+
if (!declarers.has(n))
|
|
414
|
+
declarers.set(n, stmt);
|
|
415
|
+
const refsOf = (node) => {
|
|
416
|
+
const acc = new Set();
|
|
417
|
+
const visit = (n) => {
|
|
418
|
+
if (typescript_1.default.isTypeReferenceNode(n)) {
|
|
419
|
+
let tn = n.typeName;
|
|
420
|
+
while (typescript_1.default.isQualifiedName(tn))
|
|
421
|
+
tn = tn.left;
|
|
422
|
+
acc.add(tn.text);
|
|
423
|
+
}
|
|
424
|
+
if (typescript_1.default.isTypeQueryNode(n)) {
|
|
425
|
+
let en = n.exprName;
|
|
426
|
+
while (typescript_1.default.isQualifiedName(en))
|
|
427
|
+
en = en.left;
|
|
428
|
+
acc.add(en.text);
|
|
429
|
+
}
|
|
430
|
+
typescript_1.default.forEachChild(n, visit);
|
|
431
|
+
};
|
|
432
|
+
visit(node);
|
|
433
|
+
return acc;
|
|
434
|
+
};
|
|
435
|
+
const keep = new Set([typedBridge]);
|
|
436
|
+
const queue = [typedBridge];
|
|
437
|
+
while (queue.length) {
|
|
438
|
+
const cur = queue.shift();
|
|
439
|
+
for (const name of refsOf(cur)) {
|
|
440
|
+
const d = declarers.get(name);
|
|
441
|
+
if (d && !keep.has(d)) {
|
|
442
|
+
keep.add(d);
|
|
443
|
+
queue.push(d);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return typescript_1.default.factory.updateSourceFile(sourceFile, typescript_1.default.factory.createNodeArray(statements.filter(s => keep.has(s))));
|
|
448
|
+
};
|
|
449
|
+
};
|
|
234
450
|
/**
|
|
235
451
|
* Main cleaner function.
|
|
236
452
|
* 1. Ensures top comment is present.
|
|
@@ -243,14 +459,17 @@ function cleanTsFile(src) {
|
|
|
243
459
|
const sourceFile = typescript_1.default.createSourceFile(path_1.default.basename(src), sourceCode, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
244
460
|
// Run the transformers
|
|
245
461
|
const result = typescript_1.default.transform(sourceFile, [
|
|
462
|
+
unwrapBridgeEntryTransformer,
|
|
246
463
|
resolveZodTypesTransformer,
|
|
247
464
|
removeSecondParamTransformer,
|
|
248
|
-
|
|
465
|
+
optionalUndefinedParamTransformer,
|
|
466
|
+
removeDefaultExportTransformer,
|
|
467
|
+
pruneUnreachableTransformer
|
|
249
468
|
]);
|
|
250
469
|
// Print final code
|
|
251
|
-
const
|
|
470
|
+
const header = `/* This file is auto-generated by typed-bridge. Do not edit. */`;
|
|
252
471
|
const printer = typescript_1.default.createPrinter();
|
|
253
|
-
const transformedCode =
|
|
472
|
+
const transformedCode = header + '\n' + printer.printFile(result.transformed[0]).concat(proxySnippet());
|
|
254
473
|
// Write back to the same file
|
|
255
474
|
fs_1.default.writeFileSync(src, transformedCode, 'utf-8');
|
|
256
475
|
}
|