zero-com 1.6.4 → 1.6.6

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/README.md CHANGED
@@ -7,6 +7,7 @@ The 0 bytes utility for transparently communicating client and server in full-st
7
7
  - [Usage](#usage)
8
8
  - [Transport layer](#transport-layer)
9
9
  - [Context](#context)
10
+ - [Server-to-Server Calls](#server-to-server-calls)
10
11
  - [Plugin options](#plugin-options)
11
12
  - [Complete Example](#complete-example)
12
13
 
@@ -57,7 +58,7 @@ Server side
57
58
  // server/phones.ts
58
59
  import { func } from 'zero-com';
59
60
 
60
- export const getPhones = func(async () => {
61
+ export const getPhones = func(async () => {
61
62
  // ...
62
63
  })
63
64
  ```
@@ -100,8 +101,8 @@ On the server-side, you need to create a handler that receives messages from the
100
101
  // server/api.js
101
102
  import { handle } from 'zero-com';
102
103
 
103
- const someCustomHandler = async (message) => {
104
- return await handle(message.funcId, null, message.params);
104
+ const someCustomHandler = async (message, ctx) => {
105
+ return await handle(message.funcId, ctx, message.params);
105
106
  };
106
107
 
107
108
  // Example of how to use the handler with an Express server
@@ -112,7 +113,8 @@ app.use(express.json());
112
113
 
113
114
  app.post('/api', async (req, res) => {
114
115
  try {
115
- const result = await someCustomHandler(req.body);
116
+ const ctx = { req, res };
117
+ const result = await someCustomHandler(req.body, ctx);
116
118
  res.json(result);
117
119
  } catch (error) {
118
120
  res.status(500).json({ error: error.message });
@@ -126,42 +128,90 @@ app.listen(8000, () => {
126
128
 
127
129
  ## Context
128
130
 
129
- Often you want to pass a context-related object to the server functions to have access to data like the request, response, session, etc. Zero-com provides a simple way to do this.
131
+ Often you want to access context-related data in your server functions, such as the request, response, session, etc. Zero-com provides a simple way to do this using the `context()` function.
130
132
 
131
- ### Passing Context to Server Functions
133
+ ### Accessing Context in Server Functions
132
134
 
133
- To pass context to a server function, you need to wrap the function in `func` and type the first parameter as `context`. The plugin detects this type and handles it accordingly.
135
+ To access context in a server function, call the `context<T>()` function inside your function body. The context is automatically available when the function is called via `handle()`.
134
136
 
135
137
  ```typescript
136
138
  // server/api/phones.ts
137
139
  import { func, context } from 'zero-com';
138
140
 
139
141
  type MyContext = {
140
- request: any
142
+ req: any;
143
+ res: any;
144
+ userId: string;
141
145
  }
142
146
 
143
- export const getPhones = func(async (ctx: context<MyContext>, name: string) => {
144
- // ctx is the context object passed from the server
145
- console.log(ctx.request.headers);
147
+ export const getPhones = func(async (name: string) => {
148
+ // Get the context inside the function
149
+ const ctx = context<MyContext>();
150
+
151
+ console.log('User:', ctx.userId);
152
+ console.log('Headers:', ctx.req.headers);
153
+
146
154
  // ... your code
147
155
  });
148
156
  ```
149
157
 
150
158
  ### Providing Context on the Server
151
159
 
152
- You can pass the context to `handle` when you execute the server function.
160
+ Pass the context as the second argument to `handle()`. The context will be available to the function and any nested server function calls.
153
161
 
154
162
  ```javascript
155
163
  // server/api.js
156
164
  import { handle } from 'zero-com';
157
165
 
158
- const myHandler = (request, response, message) => {
159
- const ctx = { request, response };
160
- // pass context on exec
161
- return handle(message.funcId, ctx, message.params);
162
- };
166
+ app.post('/api', async (req, res) => {
167
+ const { funcId, params } = req.body;
168
+
169
+ // Create context with request data
170
+ const ctx = {
171
+ req,
172
+ res,
173
+ userId: req.headers['x-user-id']
174
+ };
175
+
176
+ // Pass context to handle - it will be available via context()
177
+ const result = await handle(funcId, ctx, params);
178
+ res.json(result);
179
+ });
180
+ ```
181
+
182
+ ## Server-to-Server Calls
183
+
184
+ When one server function calls another server function, the call bypasses the transport layer and executes directly. Context is automatically propagated to nested calls.
185
+
186
+ ```typescript
187
+ // server/api/user.ts
188
+ import { func, context } from 'zero-com';
189
+
190
+ export const getFirstName = func(async () => {
191
+ const ctx = context<{ userId: string }>();
192
+ // ... fetch first name from database
193
+ return 'John';
194
+ });
195
+
196
+ // server/api/profile.ts
197
+ import { func, context } from 'zero-com';
198
+ import { getFirstName } from './user';
199
+
200
+ export const getFullName = func(async (lastName: string) => {
201
+ // This calls getFirstName directly (no transport layer)
202
+ // Context is automatically propagated
203
+ const firstName = await getFirstName();
204
+ return `${firstName} ${lastName}`;
205
+ });
163
206
  ```
164
207
 
208
+ When `getFullName` is called from the client:
209
+ 1. The call goes through the transport layer to the server
210
+ 2. `handle()` sets up the context
211
+ 3. `getFullName` executes and calls `getFirstName`
212
+ 4. `getFirstName` executes directly (no transport) with the same context
213
+ 5. Both functions can access `context()` with the same data
214
+
165
215
  ## Plugin options
166
216
 
167
217
  | Option | Type | Description |
@@ -225,11 +275,15 @@ call(async (funcId, params) => {
225
275
  import { func, context } from 'zero-com';
226
276
 
227
277
  type Context = {
228
- req: any,
229
- res: any
278
+ req: any;
279
+ res: any;
230
280
  }
231
281
 
232
- export const getPhones = func(async (ctx: context<Context>, name: string) => {
282
+ export const getPhones = func(async (name: string) => {
283
+ // Access context when needed
284
+ const ctx = context<Context>();
285
+ console.log('Request from:', ctx.req.ip);
286
+
233
287
  // In a real application, you would fetch this from a database
234
288
  const allPhones = [
235
289
  { name: 'iPhone 13', brand: 'Apple' },
package/lib/common.d.ts CHANGED
@@ -1,4 +1,10 @@
1
+ import type { SourceMap } from 'magic-string';
1
2
  import { CallExpression, Project, SourceFile } from 'ts-morph';
3
+ export type Replacement = {
4
+ start: number;
5
+ end: number;
6
+ content: string;
7
+ };
2
8
  export type Options = {
3
9
  development?: boolean;
4
10
  };
@@ -6,15 +12,14 @@ export type ServerFuncInfo = {
6
12
  funcId: string;
7
13
  filePath: string;
8
14
  exportName: string;
9
- requireContext: boolean;
10
15
  };
11
16
  export type ServerFuncRegistry = Map<string, Map<string, ServerFuncInfo>>;
12
17
  export declare const ZERO_COM_CLIENT_CALL = "ZERO_COM_CLIENT_CALL";
13
18
  export declare const ZERO_COM_SERVER_REGISTRY = "ZERO_COM_SERVER_REGISTRY";
19
+ export declare const ZERO_COM_CONTEXT_STORAGE = "ZERO_COM_CONTEXT_STORAGE";
14
20
  export declare const SERVER_FUNCTION_WRAPPER_NAME = "func";
15
21
  export declare const HANDLE_NAME = "handle";
16
22
  export declare const CALL_NAME = "call";
17
- export declare const CONTEXT_TYPE_NAME = "context";
18
23
  export declare const LIBRARY_NAME = "zero-com";
19
24
  export declare const FILE_EXTENSIONS: string[];
20
25
  export declare const formatFuncIdName: (funcName: string, filePath: string, line: number) => string;
@@ -28,15 +33,23 @@ export declare const resolveFilePath: (basePath: string) => string;
28
33
  export declare const createProject: () => Project;
29
34
  export declare const buildRegistry: (contextDir: string, registry: ServerFuncRegistry) => void;
30
35
  export declare const getImportedServerFunctions: (sourceFile: SourceFile, registry: ServerFuncRegistry) => Map<string, ServerFuncInfo>;
31
- export declare const transformCallSites: (sourceFile: SourceFile, importedFuncs: Map<string, ServerFuncInfo>) => boolean;
32
- export declare const transformHandleCalls: (sourceFile: SourceFile) => boolean;
33
- export declare const transformSendCalls: (sourceFile: SourceFile) => boolean;
34
- export declare const transformFuncCalls: (sourceFile: SourceFile) => boolean;
36
+ export declare const collectCallSiteReplacements: (sourceFile: SourceFile, importedFuncs: Map<string, ServerFuncInfo>) => Replacement[];
37
+ export declare const collectHandleCallReplacements: (sourceFile: SourceFile) => Replacement[];
38
+ export declare const collectSendCallReplacements: (sourceFile: SourceFile) => Replacement[];
39
+ export declare const collectFuncCallReplacements: (sourceFile: SourceFile) => Replacement[];
40
+ export declare const applyReplacementsWithMap: (source: string, replacements: Replacement[], filePath: string) => {
41
+ code: string;
42
+ map: SourceMap;
43
+ };
35
44
  export declare const appendRegistryCode: (sourceFile: SourceFile, fileRegistry: Map<string, ServerFuncInfo>) => string;
36
45
  export type TransformResult = {
37
46
  content: string;
38
47
  transformed: boolean;
48
+ map?: SourceMap;
49
+ };
50
+ export type TransformOptions = {
51
+ development?: boolean;
39
52
  };
40
- export declare const transformSourceFile: (filePath: string, content: string, registry: ServerFuncRegistry) => TransformResult;
53
+ export declare const transformSourceFile: (filePath: string, content: string, registry: ServerFuncRegistry, options?: TransformOptions) => TransformResult;
41
54
  export declare const emitToJs: (filePath: string, content: string) => string;
42
55
  export declare const applyReplacements: (code: string, compilationId: string) => string;
package/lib/common.js CHANGED
@@ -3,17 +3,18 @@ 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.applyReplacements = exports.emitToJs = exports.transformSourceFile = exports.appendRegistryCode = exports.transformFuncCalls = exports.transformSendCalls = exports.transformHandleCalls = exports.transformCallSites = exports.getImportedServerFunctions = exports.buildRegistry = exports.createProject = exports.resolveFilePath = exports.isFromLibrary = exports.getReplacements = exports.generateCompilationId = exports.formatFuncIdName = exports.FILE_EXTENSIONS = exports.LIBRARY_NAME = exports.CONTEXT_TYPE_NAME = exports.CALL_NAME = exports.HANDLE_NAME = exports.SERVER_FUNCTION_WRAPPER_NAME = exports.ZERO_COM_SERVER_REGISTRY = exports.ZERO_COM_CLIENT_CALL = void 0;
6
+ exports.applyReplacements = exports.emitToJs = exports.transformSourceFile = exports.appendRegistryCode = exports.applyReplacementsWithMap = exports.collectFuncCallReplacements = exports.collectSendCallReplacements = exports.collectHandleCallReplacements = exports.collectCallSiteReplacements = exports.getImportedServerFunctions = exports.buildRegistry = exports.createProject = exports.resolveFilePath = exports.isFromLibrary = exports.getReplacements = exports.generateCompilationId = exports.formatFuncIdName = exports.FILE_EXTENSIONS = exports.LIBRARY_NAME = exports.CALL_NAME = exports.HANDLE_NAME = exports.SERVER_FUNCTION_WRAPPER_NAME = exports.ZERO_COM_CONTEXT_STORAGE = exports.ZERO_COM_SERVER_REGISTRY = exports.ZERO_COM_CLIENT_CALL = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const magic_string_1 = __importDefault(require("magic-string"));
9
10
  const ts_morph_1 = require("ts-morph");
10
11
  // Constants
11
12
  exports.ZERO_COM_CLIENT_CALL = 'ZERO_COM_CLIENT_CALL';
12
13
  exports.ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY';
14
+ exports.ZERO_COM_CONTEXT_STORAGE = 'ZERO_COM_CONTEXT_STORAGE';
13
15
  exports.SERVER_FUNCTION_WRAPPER_NAME = 'func';
14
16
  exports.HANDLE_NAME = 'handle';
15
17
  exports.CALL_NAME = 'call';
16
- exports.CONTEXT_TYPE_NAME = 'context';
17
18
  exports.LIBRARY_NAME = 'zero-com';
18
19
  exports.FILE_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs'];
19
20
  const formatFuncIdName = (funcName, filePath, line) => {
@@ -24,7 +25,8 @@ const generateCompilationId = () => String(Math.floor(Math.random() * 1000000));
24
25
  exports.generateCompilationId = generateCompilationId;
25
26
  const getReplacements = (compilationId) => [
26
27
  { target: exports.ZERO_COM_CLIENT_CALL, replacement: `__ZERO_COM_CLIENT_CALL_${compilationId}` },
27
- { target: exports.ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${compilationId}` }
28
+ { target: exports.ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${compilationId}` },
29
+ { target: exports.ZERO_COM_CONTEXT_STORAGE, replacement: `__ZERO_COM_CONTEXT_STORAGE_${compilationId}` }
28
30
  ];
29
31
  exports.getReplacements = getReplacements;
30
32
  // Utilities
@@ -97,7 +99,6 @@ const buildRegistry = (contextDir, registry) => {
97
99
  };
98
100
  exports.buildRegistry = buildRegistry;
99
101
  const scanFileForServerFunctions = (filePath, contextDir, registry) => {
100
- var _a;
101
102
  const sourceFile = (0, exports.createProject)().createSourceFile(filePath, fs_1.default.readFileSync(filePath, 'utf8'), { overwrite: true });
102
103
  const fileRegistry = new Map();
103
104
  for (const decl of sourceFile.getVariableDeclarations()) {
@@ -115,14 +116,11 @@ const scanFileForServerFunctions = (filePath, contextDir, registry) => {
115
116
  const args = callExpr.getArguments();
116
117
  if (args.length !== 1 || args[0].getKind() !== ts_morph_1.SyntaxKind.ArrowFunction)
117
118
  continue;
118
- const params = args[0].getParameters();
119
- const requireContext = params.length > 0 && ((_a = params[0].getTypeNode()) === null || _a === void 0 ? void 0 : _a.getText().startsWith(exports.CONTEXT_TYPE_NAME)) || false;
120
119
  const exportName = decl.getName();
121
120
  fileRegistry.set(exportName, {
122
121
  funcId: (0, exports.formatFuncIdName)(exportName, path_1.default.relative(contextDir, filePath), decl.getStartLineNumber()),
123
122
  filePath,
124
123
  exportName,
125
- requireContext,
126
124
  });
127
125
  }
128
126
  if (fileRegistry.size > 0)
@@ -153,11 +151,11 @@ const getImportedServerFunctions = (sourceFile, registry) => {
153
151
  return importedFuncs;
154
152
  };
155
153
  exports.getImportedServerFunctions = getImportedServerFunctions;
156
- // Transformations
157
- const transformCallSites = (sourceFile, importedFuncs) => {
154
+ // Transformations - collect replacements instead of modifying AST
155
+ const collectCallSiteReplacements = (sourceFile, importedFuncs) => {
158
156
  if (importedFuncs.size === 0)
159
- return false;
160
- let modified = false;
157
+ return [];
158
+ const replacements = [];
161
159
  sourceFile.forEachDescendant((node) => {
162
160
  if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
163
161
  return;
@@ -167,14 +165,17 @@ const transformCallSites = (sourceFile, importedFuncs) => {
167
165
  if (!funcInfo)
168
166
  return;
169
167
  const args = callExpr.getArguments().map(a => a.getText()).join(', ');
170
- callExpr.replaceWithText(`globalThis.${exports.ZERO_COM_CLIENT_CALL}('${funcInfo.funcId}', [${args}])`);
171
- modified = true;
168
+ replacements.push({
169
+ start: callExpr.getStart(),
170
+ end: callExpr.getEnd(),
171
+ content: `globalThis.${exports.ZERO_COM_CLIENT_CALL}('${funcInfo.funcId}', [${args}])`
172
+ });
172
173
  });
173
- return modified;
174
+ return replacements;
174
175
  };
175
- exports.transformCallSites = transformCallSites;
176
- const transformHandleCalls = (sourceFile) => {
177
- let modified = false;
176
+ exports.collectCallSiteReplacements = collectCallSiteReplacements;
177
+ const collectHandleCallReplacements = (sourceFile) => {
178
+ const replacements = [];
178
179
  sourceFile.forEachDescendant((node) => {
179
180
  if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
180
181
  return;
@@ -184,18 +185,21 @@ const transformHandleCalls = (sourceFile) => {
184
185
  const args = callExpr.getArguments();
185
186
  if (args.length < 3)
186
187
  return;
187
- // Inline the logic directly using a named function for better stack traces.
188
188
  const funcId = args[0].getText();
189
189
  const ctx = args[1].getText();
190
190
  const argsArray = args[2].getText();
191
- callExpr.replaceWithText(`(function handle(__fn, __ctx, __args) { return __fn.requireContext ? __fn(__ctx, ...__args) : __fn(...__args); })(globalThis.${exports.ZERO_COM_SERVER_REGISTRY}[${funcId}], ${ctx}, ${argsArray})`);
192
- modified = true;
191
+ // Use contextStorage.run() to make context available via context() calls
192
+ replacements.push({
193
+ start: callExpr.getStart(),
194
+ end: callExpr.getEnd(),
195
+ content: `globalThis.${exports.ZERO_COM_CONTEXT_STORAGE}.run(${ctx}, () => globalThis.${exports.ZERO_COM_SERVER_REGISTRY}[${funcId}](...${argsArray}))`
196
+ });
193
197
  });
194
- return modified;
198
+ return replacements;
195
199
  };
196
- exports.transformHandleCalls = transformHandleCalls;
197
- const transformSendCalls = (sourceFile) => {
198
- let modified = false;
200
+ exports.collectHandleCallReplacements = collectHandleCallReplacements;
201
+ const collectSendCallReplacements = (sourceFile) => {
202
+ const replacements = [];
199
203
  sourceFile.forEachDescendant((node) => {
200
204
  if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
201
205
  return;
@@ -205,18 +209,21 @@ const transformSendCalls = (sourceFile) => {
205
209
  const args = callExpr.getArguments();
206
210
  if (args.length < 1)
207
211
  return;
208
- callExpr.replaceWithText(`globalThis.${exports.ZERO_COM_CLIENT_CALL} = ${args[0].getText()}`);
209
- modified = true;
212
+ replacements.push({
213
+ start: callExpr.getStart(),
214
+ end: callExpr.getEnd(),
215
+ content: `globalThis.${exports.ZERO_COM_CLIENT_CALL} = ${args[0].getText()}`
216
+ });
210
217
  });
211
- return modified;
218
+ return replacements;
212
219
  };
213
- exports.transformSendCalls = transformSendCalls;
214
- // Transform func(fn) calls to just fn.
220
+ exports.collectSendCallReplacements = collectSendCallReplacements;
221
+ // Collect func(fn) replacements to just fn.
215
222
  // The func() wrapper is only needed for type-level transformation (RemoveContextParam).
216
223
  // At runtime, we just need the raw function. The registration code (appendRegistryCode)
217
224
  // will set requireContext on the function based on compile-time analysis.
218
- const transformFuncCalls = (sourceFile) => {
219
- let modified = false;
225
+ const collectFuncCallReplacements = (sourceFile) => {
226
+ const replacements = [];
220
227
  sourceFile.forEachDescendant((node) => {
221
228
  if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
222
229
  return;
@@ -227,27 +234,41 @@ const transformFuncCalls = (sourceFile) => {
227
234
  if (args.length !== 1)
228
235
  return;
229
236
  // Replace func(fn) with just fn
230
- callExpr.replaceWithText(args[0].getText());
231
- modified = true;
237
+ replacements.push({
238
+ start: callExpr.getStart(),
239
+ end: callExpr.getEnd(),
240
+ content: args[0].getText()
241
+ });
232
242
  });
233
- return modified;
243
+ return replacements;
234
244
  };
235
- exports.transformFuncCalls = transformFuncCalls;
245
+ exports.collectFuncCallReplacements = collectFuncCallReplacements;
246
+ // Apply collected replacements using MagicString for sourcemap support
247
+ const applyReplacementsWithMap = (source, replacements, filePath) => {
248
+ const s = new magic_string_1.default(source);
249
+ // Sort replacements by start position descending to preserve positions
250
+ const sorted = [...replacements].sort((a, b) => b.start - a.start);
251
+ for (const { start, end, content } of sorted) {
252
+ s.overwrite(start, end, content);
253
+ }
254
+ return {
255
+ code: s.toString(),
256
+ map: s.generateMap({ source: filePath, includeContent: true, hires: true })
257
+ };
258
+ };
259
+ exports.applyReplacementsWithMap = applyReplacementsWithMap;
236
260
  const appendRegistryCode = (sourceFile, fileRegistry) => {
237
- // Generate registration code for each server function.
238
- // We set requireContext based on compile-time analysis of the function's first parameter type.
239
- // If the first param is context<T>, requireContext = true, otherwise false.
240
- // This allows execFunc() at runtime to know whether to inject the context as the first argument.
261
+ // Generate registration code for each server function
241
262
  const registrations = Array.from(fileRegistry.values())
242
- .map(info => `globalThis.${exports.ZERO_COM_SERVER_REGISTRY}['${info.funcId}'] = ${info.exportName};\n` +
243
- `${info.exportName}.requireContext = ${info.requireContext};`)
263
+ .map(info => `globalThis.${exports.ZERO_COM_SERVER_REGISTRY}['${info.funcId}'] = ${info.exportName};`)
244
264
  .join('\n');
245
265
  return `${sourceFile.getFullText()}
246
266
  if (!globalThis.${exports.ZERO_COM_SERVER_REGISTRY}) globalThis.${exports.ZERO_COM_SERVER_REGISTRY} = Object.create(null);
247
267
  ${registrations}`;
248
268
  };
249
269
  exports.appendRegistryCode = appendRegistryCode;
250
- const transformSourceFile = (filePath, content, registry) => {
270
+ const transformSourceFile = (filePath, content, registry, options = {}) => {
271
+ const { development = true } = options;
251
272
  const project = (0, exports.createProject)();
252
273
  const sourceFile = project.createSourceFile(filePath, content, { overwrite: true });
253
274
  const fileRegistry = registry.get(filePath);
@@ -257,17 +278,34 @@ const transformSourceFile = (filePath, content, registry) => {
257
278
  if (isServerFunctionFile) {
258
279
  fileRegistry.forEach((_, name) => importedFuncs.delete(name));
259
280
  }
260
- const callsTransformed = (0, exports.transformCallSites)(sourceFile, importedFuncs);
261
- const handleTransformed = (0, exports.transformHandleCalls)(sourceFile);
262
- const sendTransformed = (0, exports.transformSendCalls)(sourceFile);
263
- // Transform func() calls in files that define server functions.
264
- // This strips the func() wrapper since it's only needed for types.
281
+ // Collect all replacements
282
+ const replacements = [];
283
+ // Always collect client call site replacements (needed in both dev and prod)
284
+ replacements.push(...(0, exports.collectCallSiteReplacements)(sourceFile, importedFuncs));
285
+ // In production mode, collect handle(), call(), and func() replacements.
286
+ // In development mode, these work at runtime via ZERO_COM_DEV_MODE flag.
287
+ if (!development) {
288
+ replacements.push(...(0, exports.collectHandleCallReplacements)(sourceFile));
289
+ replacements.push(...(0, exports.collectSendCallReplacements)(sourceFile));
290
+ }
291
+ // Handle server function files
265
292
  if (isServerFunctionFile) {
266
- (0, exports.transformFuncCalls)(sourceFile);
293
+ // In production mode, strip func() wrappers
294
+ if (!development) {
295
+ replacements.push(...(0, exports.collectFuncCallReplacements)(sourceFile));
296
+ }
297
+ // Apply replacements and append registry code
298
+ if (replacements.length > 0) {
299
+ const { code, map } = (0, exports.applyReplacementsWithMap)(content, replacements, filePath);
300
+ // Create a new source file from the transformed code to append registry
301
+ const transformedSourceFile = project.createSourceFile(filePath + '.transformed', code, { overwrite: true });
302
+ return { content: (0, exports.appendRegistryCode)(transformedSourceFile, fileRegistry), transformed: true, map };
303
+ }
267
304
  return { content: (0, exports.appendRegistryCode)(sourceFile, fileRegistry), transformed: true };
268
305
  }
269
- if (callsTransformed || handleTransformed || sendTransformed) {
270
- return { content: sourceFile.getFullText(), transformed: true };
306
+ if (replacements.length > 0) {
307
+ const { code, map } = (0, exports.applyReplacementsWithMap)(content, replacements, filePath);
308
+ return { content: code, transformed: true, map };
271
309
  }
272
310
  return { content, transformed: false };
273
311
  };
package/lib/rollup.js CHANGED
@@ -42,15 +42,16 @@ function zeroComRollupPlugin(options = {}) {
42
42
  });
43
43
  },
44
44
  load(id) {
45
- var _a, _b;
45
+ var _a, _b, _c;
46
46
  if (!((_b = (_a = this.getModuleInfo(id)) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.needsTransform) || !fs_1.default.existsSync(id))
47
47
  return null;
48
48
  const content = fs_1.default.readFileSync(id, 'utf8');
49
- const result = (0, common_1.transformSourceFile)(id, content, registry);
49
+ const result = (0, common_1.transformSourceFile)(id, content, registry, { development });
50
50
  if (!result.transformed)
51
51
  return null;
52
52
  console.log(`[ZeroComRollupPlugin] Transformed: ${path_1.default.relative(process.cwd(), id)}`);
53
- return (0, common_1.emitToJs)(id, result.content);
53
+ const code = (0, common_1.emitToJs)(id, result.content);
54
+ return { code, map: (_c = result.map) !== null && _c !== void 0 ? _c : null };
54
55
  },
55
56
  renderChunk(code) {
56
57
  if (development)
package/lib/runtime.d.ts CHANGED
@@ -2,14 +2,13 @@ declare global {
2
2
  var ZERO_COM_SERVER_REGISTRY: {
3
3
  [funcId: string]: (...args: any[]) => any;
4
4
  };
5
- var ZERO_COM_CLIENT_CALL: (funcId: string, args: any[]) => Promise<any>;
5
+ var ZERO_COM_CLIENT_CALL: (funcId: string, args: any[]) => any;
6
+ var ZERO_COM_CONTEXT_STORAGE: {
7
+ run: <T>(ctx: any, fn: () => T) => T;
8
+ getStore: () => any;
9
+ } | undefined;
6
10
  }
7
- declare const contextBrand: unique symbol;
8
- export type context<T = unknown> = T & {
9
- readonly [contextBrand]: never;
10
- };
11
- type RemoveContextParam<F> = F extends (ctx: infer C, ...args: infer A) => infer R ? C extends context<unknown> ? (...args: A) => R : F : F;
12
- export declare function func<F extends (...args: any[]) => any>(fn: F): RemoveContextParam<F>;
13
- export declare const handle: (_funcId: string, _ctx: any, _args: any[]) => any;
14
- export declare const call: (_fn: (funcId: string, args: any[]) => Promise<any>) => void;
15
- export {};
11
+ export declare function context<T = unknown>(): T;
12
+ export declare function func<F extends (...args: any[]) => any>(fn: F): F;
13
+ export declare const handle: (funcId: string, ctx: any, args: any[]) => any;
14
+ export declare const call: (fn: (funcId: string, args: any[]) => any) => void;
package/lib/runtime.js CHANGED
@@ -1,21 +1,78 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.call = exports.handle = void 0;
4
+ exports.context = context;
4
5
  exports.func = func;
5
- // Implementation
6
- // User-facing function - transformed by plugin to just the inner function.
7
- // The plugin also appends code to register the function and set requireContext based on
8
- // compile-time analysis of whether the first parameter is context<T>.
9
- function func(_fn) {
10
- throw new Error('func() was not transformed. Ensure the zero-com plugin is configured.');
6
+ // Context storage - only available on server (Node.js)
7
+ // Lazily initialized to avoid importing async_hooks on client
8
+ function getContextStorage() {
9
+ if (!globalThis.ZERO_COM_CONTEXT_STORAGE) {
10
+ try {
11
+ // Dynamic import to avoid bundling async_hooks for browser
12
+ const { AsyncLocalStorage } = require('async_hooks');
13
+ globalThis.ZERO_COM_CONTEXT_STORAGE = new AsyncLocalStorage();
14
+ }
15
+ catch (_a) {
16
+ // Browser environment - context storage not available
17
+ globalThis.ZERO_COM_CONTEXT_STORAGE = undefined;
18
+ }
19
+ }
20
+ return globalThis.ZERO_COM_CONTEXT_STORAGE;
11
21
  }
12
- // User-facing function - transformed by plugin to inline code that checks requireContext and calls the function
13
- const handle = (_funcId, _ctx, _args) => {
14
- throw new Error('handle() was not transformed. Ensure the zero-com plugin is configured.');
22
+ // Get the current context - call this inside server functions
23
+ function context() {
24
+ const storage = getContextStorage();
25
+ if (!storage) {
26
+ throw new Error('context() is only available on the server');
27
+ }
28
+ const ctx = storage.getStore();
29
+ if (ctx === undefined) {
30
+ throw new Error('context() called outside of a server function');
31
+ }
32
+ return ctx;
33
+ }
34
+ // Default server-side implementation: call directly from registry
35
+ // This enables server functions to call other server functions without transport
36
+ if (typeof globalThis.ZERO_COM_CLIENT_CALL === 'undefined') {
37
+ globalThis.ZERO_COM_CLIENT_CALL = (funcId, args) => {
38
+ var _a;
39
+ const storage = getContextStorage();
40
+ if (!storage) {
41
+ throw new Error('Server function called on client without transport configured. Call call() first.');
42
+ }
43
+ const ctx = storage.getStore();
44
+ if (ctx === undefined) {
45
+ throw new Error('Server function called outside of request context');
46
+ }
47
+ const fn = (_a = globalThis.ZERO_COM_SERVER_REGISTRY) === null || _a === void 0 ? void 0 : _a[funcId];
48
+ if (!fn)
49
+ throw new Error(`Function not found: ${funcId}`);
50
+ return fn(...args);
51
+ };
52
+ }
53
+ // func() just returns the function as-is
54
+ // In production mode: transformed by plugin to just the inner function
55
+ function func(fn) {
56
+ return fn;
57
+ }
58
+ // handle() stores context in AsyncLocalStorage and calls the function
59
+ // In production mode: transformed by plugin to inline code
60
+ const handle = (funcId, ctx, args) => {
61
+ var _a;
62
+ const fn = (_a = globalThis.ZERO_COM_SERVER_REGISTRY) === null || _a === void 0 ? void 0 : _a[funcId];
63
+ if (!fn) {
64
+ throw new Error(`Function not found in registry: ${funcId}`);
65
+ }
66
+ const storage = getContextStorage();
67
+ if (!storage) {
68
+ throw new Error('handle() is only available on the server');
69
+ }
70
+ return storage.run(ctx, () => fn(...args));
15
71
  };
16
72
  exports.handle = handle;
17
- // User-facing function - transformed by plugin to globalThis.ZERO_COM_CLIENT_CALL = fn
18
- const call = (_fn) => {
19
- throw new Error('call() was not transformed. Ensure the zero-com plugin is configured.');
73
+ // Client calls this to set up transport (overrides default server-side behavior)
74
+ // In production mode: transformed by plugin to assignment
75
+ const call = (fn) => {
76
+ globalThis.ZERO_COM_CLIENT_CALL = fn;
20
77
  };
21
78
  exports.call = call;
@@ -2,5 +2,6 @@ import type { LoaderContext } from 'webpack';
2
2
  import { ServerFuncRegistry } from './common';
3
3
  export interface ZeroComLoaderOptions {
4
4
  registry: ServerFuncRegistry;
5
+ development: boolean;
5
6
  }
6
- export default function zeroComLoader(this: LoaderContext<ZeroComLoaderOptions>, source: string): string;
7
+ export default function zeroComLoader(this: LoaderContext<ZeroComLoaderOptions>, source: string): void;
@@ -9,10 +9,13 @@ const common_1 = require("./common");
9
9
  function zeroComLoader(source) {
10
10
  const options = this.getOptions();
11
11
  const filePath = this.resourcePath;
12
- const result = (0, common_1.transformSourceFile)(filePath, source, options.registry);
12
+ const result = (0, common_1.transformSourceFile)(filePath, source, options.registry, {
13
+ development: options.development
14
+ });
13
15
  if (!result.transformed) {
14
- return source;
16
+ this.callback(null, source);
17
+ return;
15
18
  }
16
19
  console.log(`[ZeroComWebpackPlugin] Transformed: ${path_1.default.basename(filePath)}`);
17
- return result.content;
20
+ this.callback(null, result.content, result.map);
18
21
  }
package/lib/webpack.js CHANGED
@@ -24,7 +24,7 @@ class ZeroComWebpackPlugin {
24
24
  enforce: 'pre',
25
25
  use: [{
26
26
  loader: loaderPath,
27
- options: { registry: this.registry }
27
+ options: { registry: this.registry, development: this.options.development }
28
28
  }]
29
29
  };
30
30
  compiler.options.module.rules.unshift(loaderRule);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-com",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "main": "index.js",
5
5
  "repository": "https://github.com/yosbelms/zero-com",
6
6
  "keywords": [
@@ -22,6 +22,7 @@
22
22
  "license": "MIT",
23
23
  "description": "",
24
24
  "dependencies": {
25
+ "magic-string": "^0.30.21",
25
26
  "minimatch": "^10.0.1",
26
27
  "ts-morph": "^26.0.0"
27
28
  },