zero-com 1.6.5 → 1.7.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.
@@ -2,7 +2,8 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(npm run build:*)",
5
- "Bash(npm test)"
5
+ "Bash(npm test)",
6
+ "Bash(grep:*)"
6
7
  ]
7
8
  }
8
9
  }
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
@@ -12,15 +12,14 @@ export type ServerFuncInfo = {
12
12
  funcId: string;
13
13
  filePath: string;
14
14
  exportName: string;
15
- requireContext: boolean;
16
15
  };
17
16
  export type ServerFuncRegistry = Map<string, Map<string, ServerFuncInfo>>;
18
17
  export declare const ZERO_COM_CLIENT_CALL = "ZERO_COM_CLIENT_CALL";
19
18
  export declare const ZERO_COM_SERVER_REGISTRY = "ZERO_COM_SERVER_REGISTRY";
19
+ export declare const ZERO_COM_CONTEXT_STORAGE = "ZERO_COM_CONTEXT_STORAGE";
20
20
  export declare const SERVER_FUNCTION_WRAPPER_NAME = "func";
21
21
  export declare const HANDLE_NAME = "handle";
22
22
  export declare const CALL_NAME = "call";
23
- export declare const CONTEXT_TYPE_NAME = "context";
24
23
  export declare const LIBRARY_NAME = "zero-com";
25
24
  export declare const FILE_EXTENSIONS: string[];
26
25
  export declare const formatFuncIdName: (funcName: string, filePath: string, line: number) => string;
package/lib/common.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.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.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
9
  const magic_string_1 = __importDefault(require("magic-string"));
@@ -11,10 +11,10 @@ const ts_morph_1 = require("ts-morph");
11
11
  // Constants
12
12
  exports.ZERO_COM_CLIENT_CALL = 'ZERO_COM_CLIENT_CALL';
13
13
  exports.ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY';
14
+ exports.ZERO_COM_CONTEXT_STORAGE = 'ZERO_COM_CONTEXT_STORAGE';
14
15
  exports.SERVER_FUNCTION_WRAPPER_NAME = 'func';
15
16
  exports.HANDLE_NAME = 'handle';
16
17
  exports.CALL_NAME = 'call';
17
- exports.CONTEXT_TYPE_NAME = 'context';
18
18
  exports.LIBRARY_NAME = 'zero-com';
19
19
  exports.FILE_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs'];
20
20
  const formatFuncIdName = (funcName, filePath, line) => {
@@ -25,7 +25,8 @@ const generateCompilationId = () => String(Math.floor(Math.random() * 1000000));
25
25
  exports.generateCompilationId = generateCompilationId;
26
26
  const getReplacements = (compilationId) => [
27
27
  { target: exports.ZERO_COM_CLIENT_CALL, replacement: `__ZERO_COM_CLIENT_CALL_${compilationId}` },
28
- { 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}` }
29
30
  ];
30
31
  exports.getReplacements = getReplacements;
31
32
  // Utilities
@@ -98,7 +99,6 @@ const buildRegistry = (contextDir, registry) => {
98
99
  };
99
100
  exports.buildRegistry = buildRegistry;
100
101
  const scanFileForServerFunctions = (filePath, contextDir, registry) => {
101
- var _a;
102
102
  const sourceFile = (0, exports.createProject)().createSourceFile(filePath, fs_1.default.readFileSync(filePath, 'utf8'), { overwrite: true });
103
103
  const fileRegistry = new Map();
104
104
  for (const decl of sourceFile.getVariableDeclarations()) {
@@ -116,14 +116,11 @@ const scanFileForServerFunctions = (filePath, contextDir, registry) => {
116
116
  const args = callExpr.getArguments();
117
117
  if (args.length !== 1 || args[0].getKind() !== ts_morph_1.SyntaxKind.ArrowFunction)
118
118
  continue;
119
- const params = args[0].getParameters();
120
- const requireContext = params.length > 0 && ((_a = params[0].getTypeNode()) === null || _a === void 0 ? void 0 : _a.getText().startsWith(exports.CONTEXT_TYPE_NAME)) || false;
121
119
  const exportName = decl.getName();
122
120
  fileRegistry.set(exportName, {
123
121
  funcId: (0, exports.formatFuncIdName)(exportName, path_1.default.relative(contextDir, filePath), decl.getStartLineNumber()),
124
122
  filePath,
125
123
  exportName,
126
- requireContext,
127
124
  });
128
125
  }
129
126
  if (fileRegistry.size > 0)
@@ -191,10 +188,11 @@ const collectHandleCallReplacements = (sourceFile) => {
191
188
  const funcId = args[0].getText();
192
189
  const ctx = args[1].getText();
193
190
  const argsArray = args[2].getText();
191
+ // Use contextStorage.run() to make context available via context() calls
194
192
  replacements.push({
195
193
  start: callExpr.getStart(),
196
194
  end: callExpr.getEnd(),
197
- content: `((__fn) => __fn.requireContext ? __fn(${ctx}, ...${argsArray}) : __fn(...${argsArray}))(globalThis.${exports.ZERO_COM_SERVER_REGISTRY}[${funcId}])`
195
+ content: `globalThis.${exports.ZERO_COM_CONTEXT_STORAGE}.run(${ctx}, () => globalThis.${exports.ZERO_COM_SERVER_REGISTRY}[${funcId}](...${argsArray}))`
198
196
  });
199
197
  });
200
198
  return replacements;
@@ -260,13 +258,9 @@ const applyReplacementsWithMap = (source, replacements, filePath) => {
260
258
  };
261
259
  exports.applyReplacementsWithMap = applyReplacementsWithMap;
262
260
  const appendRegistryCode = (sourceFile, fileRegistry) => {
263
- // Generate registration code for each server function.
264
- // We set requireContext based on compile-time analysis of the function's first parameter type.
265
- // If the first param is context<T>, requireContext = true, otherwise false.
266
- // This allows handle() at runtime to know whether to inject the context as the first argument.
261
+ // Generate registration code for each server function
267
262
  const registrations = Array.from(fileRegistry.values())
268
- .map(info => `globalThis.${exports.ZERO_COM_SERVER_REGISTRY}['${info.funcId}'] = ${info.exportName};\n` +
269
- `${info.exportName}.requireContext = ${info.requireContext};`)
263
+ .map(info => `globalThis.${exports.ZERO_COM_SERVER_REGISTRY}['${info.funcId}'] = ${info.exportName};`)
270
264
  .join('\n');
271
265
  return `${sourceFile.getFullText()}
272
266
  if (!globalThis.${exports.ZERO_COM_SERVER_REGISTRY}) globalThis.${exports.ZERO_COM_SERVER_REGISTRY} = Object.create(null);
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>;
11
+ export declare function context<T = unknown>(): T;
12
+ export declare function func<F extends (...args: any[]) => any>(fn: F): F;
13
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 {};
14
+ export declare const call: (fn: (funcId: string, args: any[]) => any) => void;
package/lib/runtime.js CHANGED
@@ -1,31 +1,77 @@
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
- // In development mode: works at runtime, returns the function as-is.
7
- // In production mode: transformed by plugin to just the inner function.
8
- // The plugin appends code to register the function and set requireContext.
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;
21
+ }
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
9
55
  function func(fn) {
10
56
  return fn;
11
57
  }
12
- // In development mode: works at runtime, looks up function and calls it.
13
- // In production mode: transformed by plugin to inline code.
58
+ // handle() stores context in AsyncLocalStorage and calls the function
59
+ // In production mode: transformed by plugin to inline code
14
60
  const handle = (funcId, ctx, args) => {
15
- const fn = globalThis.ZERO_COM_SERVER_REGISTRY[funcId];
61
+ var _a;
62
+ const fn = (_a = globalThis.ZERO_COM_SERVER_REGISTRY) === null || _a === void 0 ? void 0 : _a[funcId];
16
63
  if (!fn) {
17
64
  throw new Error(`Function not found in registry: ${funcId}`);
18
65
  }
19
- if (fn.requireContext) {
20
- return fn(ctx, ...args);
21
- }
22
- else {
23
- return fn(...args);
66
+ const storage = getContextStorage();
67
+ if (!storage) {
68
+ throw new Error('handle() is only available on the server');
24
69
  }
70
+ return storage.run(ctx, () => fn(...args));
25
71
  };
26
72
  exports.handle = handle;
27
- // In development mode: works at runtime, sets the client call handler.
28
- // In production mode: transformed by plugin to assignment.
73
+ // Client calls this to set up transport (overrides default server-side behavior)
74
+ // In production mode: transformed by plugin to assignment
29
75
  const call = (fn) => {
30
76
  globalThis.ZERO_COM_CLIENT_CALL = fn;
31
77
  };
@@ -0,0 +1,6 @@
1
+ import type { LoaderContext } from 'webpack';
2
+ export interface TurbopackLoaderOptions {
3
+ development?: boolean;
4
+ rootDir?: string;
5
+ }
6
+ export default function turbopackLoader(this: LoaderContext<TurbopackLoaderOptions>, source: string): void;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = turbopackLoader;
7
+ const path_1 = __importDefault(require("path"));
8
+ const common_1 = require("./common");
9
+ // Module-level cache: registry is built once and reused across invocations
10
+ let cachedRegistry = null;
11
+ let cachedRootDir = null;
12
+ function turbopackLoader(source) {
13
+ var _a;
14
+ const options = this.getOptions();
15
+ const filePath = this.resourcePath;
16
+ const rootDir = options.rootDir || this.rootContext || process.cwd();
17
+ const development = (_a = options.development) !== null && _a !== void 0 ? _a : true;
18
+ // Lazily build and cache the registry on first invocation or if rootDir changes
19
+ if (!cachedRegistry || cachedRootDir !== rootDir) {
20
+ cachedRegistry = new Map();
21
+ cachedRootDir = rootDir;
22
+ (0, common_1.buildRegistry)(rootDir, cachedRegistry);
23
+ console.log(`[TurbopackLoader] Found ${cachedRegistry.size} files with server functions`);
24
+ }
25
+ const result = (0, common_1.transformSourceFile)(filePath, source, cachedRegistry, { development });
26
+ if (!result.transformed) {
27
+ this.callback(null, source);
28
+ return;
29
+ }
30
+ console.log(`[TurbopackLoader] Transformed: ${path_1.default.basename(filePath)}`);
31
+ this.callback(null, result.content, result.map);
32
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "zero-com",
3
- "version": "1.6.5",
3
+ "version": "1.7.1",
4
4
  "main": "index.js",
5
5
  "repository": "https://github.com/yosbelms/zero-com",
6
6
  "keywords": [
7
7
  "webpack",
8
+ "turbopack",
8
9
  "api",
9
10
  "node",
10
11
  "http",