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 +74 -20
- package/lib/common.d.ts +20 -7
- package/lib/common.js +88 -50
- package/lib/rollup.js +4 -3
- package/lib/runtime.d.ts +9 -10
- package/lib/runtime.js +69 -12
- package/lib/webpack-loader.d.ts +2 -1
- package/lib/webpack-loader.js +6 -3
- package/lib/webpack.js +1 -1
- package/package.json +2 -1
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,
|
|
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
|
|
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
|
|
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
|
-
###
|
|
133
|
+
### Accessing Context in Server Functions
|
|
132
134
|
|
|
133
|
-
To
|
|
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
|
-
|
|
142
|
+
req: any;
|
|
143
|
+
res: any;
|
|
144
|
+
userId: string;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
export const getPhones = func(async (
|
|
144
|
-
//
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
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 (
|
|
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
|
|
32
|
-
export declare const
|
|
33
|
-
export declare const
|
|
34
|
-
export declare const
|
|
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.
|
|
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
|
|
154
|
+
// Transformations - collect replacements instead of modifying AST
|
|
155
|
+
const collectCallSiteReplacements = (sourceFile, importedFuncs) => {
|
|
158
156
|
if (importedFuncs.size === 0)
|
|
159
|
-
return
|
|
160
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
|
174
|
+
return replacements;
|
|
174
175
|
};
|
|
175
|
-
exports.
|
|
176
|
-
const
|
|
177
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
|
198
|
+
return replacements;
|
|
195
199
|
};
|
|
196
|
-
exports.
|
|
197
|
-
const
|
|
198
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
218
|
+
return replacements;
|
|
212
219
|
};
|
|
213
|
-
exports.
|
|
214
|
-
//
|
|
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
|
|
219
|
-
|
|
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
|
-
|
|
231
|
-
|
|
237
|
+
replacements.push({
|
|
238
|
+
start: callExpr.getStart(),
|
|
239
|
+
end: callExpr.getEnd(),
|
|
240
|
+
content: args[0].getText()
|
|
241
|
+
});
|
|
232
242
|
});
|
|
233
|
-
return
|
|
243
|
+
return replacements;
|
|
234
244
|
};
|
|
235
|
-
exports.
|
|
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}
|
|
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
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
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
|
-
|
|
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 (
|
|
270
|
-
|
|
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
|
-
|
|
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[]) =>
|
|
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
|
|
8
|
-
export
|
|
9
|
-
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
package/lib/webpack-loader.d.ts
CHANGED
|
@@ -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):
|
|
7
|
+
export default function zeroComLoader(this: LoaderContext<ZeroComLoaderOptions>, source: string): void;
|
package/lib/webpack-loader.js
CHANGED
|
@@ -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
|
-
|
|
16
|
+
this.callback(null, source);
|
|
17
|
+
return;
|
|
15
18
|
}
|
|
16
19
|
console.log(`[ZeroComWebpackPlugin] Transformed: ${path_1.default.basename(filePath)}`);
|
|
17
|
-
|
|
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.
|
|
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
|
},
|