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.
@@ -1,8 +1,13 @@
1
1
  import { Application, Request, Response } from 'express';
2
2
  import { Server } from 'http';
3
- type Bridge = {
4
- [key: string]: (...args: any[]) => Promise<any>;
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
  };
@@ -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.errors)) {
140
- const keyPath = error.errors[0].path.join('/');
141
- const errorMessage = (keyPath ? keyPath + ': ' : '') + error.errors[0].message;
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)
@@ -7,6 +7,7 @@ interface config {
7
7
  contextOnError: boolean;
8
8
  };
9
9
  responseDelay: number;
10
+ maxToolOutputChars: number;
10
11
  }
11
12
  export declare const config: config;
12
13
  export {};
@@ -9,5 +9,6 @@ exports.config = {
9
9
  argsOnError: true,
10
10
  contextOnError: true
11
11
  },
12
- responseDelay: 0
12
+ responseDelay: 0,
13
+ maxToolOutputChars: 0
13
14
  };
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 typedBridgeConfig = {
12
+ type TypedBridgeConfig = {
13
13
  host: string
14
- headers: { [key: string]: string }
15
- onResponse: (res: Response) => void
14
+ headers: Record<string, string>
15
+ onResponse: (response: Response) => void
16
16
  }
17
17
 
18
- export const typedBridgeConfig: typedBridgeConfig = {
18
+ export const typedBridgeConfig: TypedBridgeConfig = {
19
19
  host: '',
20
20
  headers: { 'Content-Type': 'application/json' },
21
- onResponse: (res: Response) => {}
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: any) => {
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 typeof _default
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
- * Remove "export { _default as default }" if it exists.
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 removeDefaultExportTransformer = context => {
229
+ const optionalUndefinedParamTransformer = context => {
212
230
  return sourceFile => {
213
231
  function visitor(node) {
214
- // Look for `export { _default as default }` and drop it
215
- if (typescript_1.default.isExportDeclaration(node) && node.exportClause && typescript_1.default.isNamedExports(node.exportClause)) {
216
- const [el] = node.exportClause.elements;
217
- if (node.exportClause.elements.length === 1 &&
218
- el.propertyName?.text === '_default' &&
219
- el.name.text === 'default') {
220
- return undefined;
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
- const newStmt = typescript_1.default.visitNode(stmt, visitor);
228
- if (newStmt)
229
- updatedStatements.push(newStmt);
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
- removeDefaultExportTransformer
465
+ optionalUndefinedParamTransformer,
466
+ removeDefaultExportTransformer,
467
+ pruneUnreachableTransformer
249
468
  ]);
250
469
  // Print final code
251
- const eslintDisable = `/* eslint-disable */`;
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 = eslintDisable + '\n' + printer.printFile(result.transformed[0]).concat(proxySnippet());
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
  }