zero-com 0.0.8 → 1.2.0

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
@@ -1,6 +1,6 @@
1
1
  # Zero-com
2
2
 
3
- It is a zero-byte no-lib utility for transparently communicating client-side and server-side modules residing in the same full-stack project.
3
+ A ~420 Bytes utility for transparently communicating client and server in full-stack projects through compile-time code transformation, with end-to-end static type checking.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -27,10 +27,6 @@ module.exports = {
27
27
  plugins: [
28
28
  new ZeroComWebpackPlugin({
29
29
  development: true,
30
- patterns: {
31
- client: 'src/client/**',
32
- server: 'src/server/api/**',
33
- },
34
30
  }),
35
31
  ],
36
32
  };
@@ -49,10 +45,6 @@ export default {
49
45
  plugins: [
50
46
  zeroComRollupPlugin({
51
47
  development: true,
52
- patterns: {
53
- client: 'src/client/**',
54
- server: 'src/server/api/**',
55
- },
56
48
  }),
57
49
  ],
58
50
  };
@@ -61,16 +53,17 @@ export default {
61
53
  The above code will identify all the references from client-side code to the server-side files and will tranform the modules to comunicate through your defined transport layer. The only callable functions in the server-side modules will be the exported async functions. See the example below.
62
54
 
63
55
  Server side
64
- ```js
56
+ ```ts
65
57
  // server/phones.ts
66
- export async function getPhones() { }
58
+ import { func } from 'zero-com';
67
59
 
68
- // or
69
- export const getPhones = async () => { }
60
+ export const getPhones = func(async () => {
61
+ // ...
62
+ })
70
63
  ```
71
64
 
72
65
  Client side
73
- ```js
66
+ ```tsx
74
67
  // client/phones.tsx
75
68
  import { getPhones } '../server/phones'
76
69
  ```
@@ -79,28 +72,15 @@ import { getPhones } '../server/phones'
79
72
 
80
73
  Zero-com does not define any transport layer, it is up to you to create one or reuse your own. This means you have complete control over how data is sent between the client and server.
81
74
 
82
- ### Communication Flow
83
-
84
- The following diagram illustrates the communication flow between the client and server:
85
-
86
- ```
87
- +--------+ +-----------------------------+ +-------------+
88
- | Client |----->| window.ZERO_COM_CLIENT_SEND |----->| Your Server |
89
- +--------+ +-----------------------------+ +-------------+
90
- |
91
- v
92
- +--------+ +-------------------------+ +-------------------+
93
- | Client |<-----| (Your custom transport) |<-----| someCustomHandler |
94
- +--------+ +-------------------------+ +-------------------+
95
- ```
96
-
97
75
  ### Client-side
98
76
 
99
- All messages from the client-side will be sent using the `window.ZERO_COM_CLIENT_SEND` function. You need to define this function in your client-side code.
77
+ All messages from the client-side will be sent using the transport function you define. Import `call` from `zero-com` and pass your transport function.
100
78
 
101
79
  ```javascript
102
80
  // client/transport.js
103
- window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
81
+ import { call } from 'zero-com';
82
+
83
+ call(async (funcId, params) => {
104
84
  const response = await fetch('http://localhost:8000/api', {
105
85
  method: 'POST',
106
86
  headers: {
@@ -109,24 +89,19 @@ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
109
89
  body: JSON.stringify({ funcId, params }),
110
90
  });
111
91
  return await response.json();
112
- };
92
+ });
113
93
  ```
114
94
 
115
95
  ### Server-side
116
96
 
117
- On the server-side, you need to create a handler that receives messages from the client, executes the corresponding function, and returns the result. The `global.ZERO_COM_SERVER_REGISTRY` object contains all the server functions that can be called from the client.
97
+ On the server-side, you need to create a handler that receives messages from the client, executes the corresponding function, and returns the result. Import `handle` from `zero-com` and call it with the function ID, context, and arguments.
118
98
 
119
99
  ```javascript
120
100
  // server/api.js
121
- import { execServerFn } from 'zero-com';
101
+ import { handle } from 'zero-com';
122
102
 
123
103
  const someCustomHandler = async (message) => {
124
- const func = global.ZERO_COM_SERVER_REGISTRY[message.funcId];
125
- if (func) {
126
- return await execServerFn(func, message.params);
127
- } else {
128
- throw new Error(`Function with id ${message.funcId} not found.`);
129
- }
104
+ return await handle(message.funcId, null, message.params);
130
105
  };
131
106
 
132
107
  // Example of how to use the handler with an Express server
@@ -155,13 +130,17 @@ Often you want to pass a context-related object to the server functions to have
155
130
 
156
131
  ### Passing Context to Server Functions
157
132
 
158
- To pass context to a server function, you need to wrap the function in `serverFn` and receive the context as the first parameter.
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.
159
134
 
160
- ```javascript
161
- // server/api/phones.js
162
- import { serverFn } from 'zero-com';
135
+ ```typescript
136
+ // server/api/phones.ts
137
+ import { func, context } from 'zero-com';
138
+
139
+ type MyContext = {
140
+ request: any
141
+ }
163
142
 
164
- export const getPhones = serverFn(async (ctx, name) => {
143
+ export const getPhones = func(async (ctx: context<MyContext>, name: string) => {
165
144
  // ctx is the context object passed from the server
166
145
  console.log(ctx.request.headers);
167
146
  // ... your code
@@ -170,17 +149,16 @@ export const getPhones = serverFn(async (ctx, name) => {
170
149
 
171
150
  ### Providing Context on the Server
172
151
 
173
- You can pass the context to `execServerFn` when you execute the server function.
152
+ You can pass the context to `handle` when you execute the server function.
174
153
 
175
154
  ```javascript
176
155
  // server/api.js
177
- import { execServerFn } from 'zero-com';
156
+ import { handle } from 'zero-com';
178
157
 
179
158
  const myHandler = (request, response, message) => {
180
159
  const ctx = { request, response };
181
- const func = global.ZERO_COM_SERVER_REGISTRY[message.funcId];
182
160
  // pass context on exec
183
- return execServerFn(func, ctx, message.params);
161
+ return handle(message.funcId, ctx, message.params);
184
162
  };
185
163
  ```
186
164
 
@@ -189,9 +167,6 @@ const myHandler = (request, response, message) => {
189
167
  | Option | Type | Description |
190
168
  |-------------|-----------|-----------------------------------------------------------------------------|
191
169
  | `development` | `boolean` | If `false`, will add internal variable renaming to the final bundle. |
192
- | `patterns` | `object` | |
193
- | `patterns.client` | `string` | A glob pattern to identify client-side files. |
194
- | `patterns.server` | `string` | A glob pattern to identify server-side files. |
195
170
 
196
171
  ## Complete Example
197
172
 
@@ -206,17 +181,17 @@ Here's a complete example of how to use Zero-com in a project.
206
181
  ├── rollup.config.js
207
182
  └── src
208
183
  ├── client
209
- │ ├── index.js
210
- │ └── transport.js
184
+ │ ├── index.ts
185
+ │ └── transport.ts
211
186
  └── server
212
187
  └── api
213
- └── phones.js
188
+ └── phones.ts
214
189
  ```
215
190
 
216
191
  ### Client-side
217
192
 
218
- ```javascript
219
- // src/client/index.js
193
+ ```typescript
194
+ // src/client/index.ts
220
195
  import { getPhones } from '../../server/api/phones';
221
196
 
222
197
  async function main() {
@@ -227,9 +202,11 @@ async function main() {
227
202
  main();
228
203
  ```
229
204
 
230
- ```javascript
231
- // src/client/transport.js
232
- window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
205
+ ```typescript
206
+ // src/client/transport.ts
207
+ import { call } from 'zero-com';
208
+
209
+ call(async (funcId, params) => {
233
210
  const response = await fetch('http://localhost:8000/api', {
234
211
  method: 'POST',
235
212
  headers: {
@@ -238,16 +215,21 @@ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
238
215
  body: JSON.stringify({ funcId, params }),
239
216
  });
240
217
  return await response.json();
241
- };
218
+ });
242
219
  ```
243
220
 
244
221
  ### Server-side
245
222
 
246
- ```javascript
247
- // src/server/api/phones.js
248
- import { serverFn } from 'zero-com';
223
+ ```typescript
224
+ // src/server/api/phones.ts
225
+ import { func, context } from 'zero-com';
249
226
 
250
- export const getPhones = serverFn(async (ctx, name) => {
227
+ type Context = {
228
+ req: any,
229
+ res: any
230
+ }
231
+
232
+ export const getPhones = func(async (ctx: context<Context>, name: string) => {
251
233
  // In a real application, you would fetch this from a database
252
234
  const allPhones = [
253
235
  { name: 'iPhone 13', brand: 'Apple' },
@@ -260,10 +242,10 @@ export const getPhones = serverFn(async (ctx, name) => {
260
242
 
261
243
  ### Server
262
244
 
263
- ```javascript
264
- // server.js
245
+ ```typescript
246
+ // server.ts
265
247
  import express from 'express';
266
- import { execServerFn } from 'zero-com';
248
+ import { handle } from 'zero-com';
267
249
  import './src/server/api/phones.js'; // Make sure to import the server-side modules
268
250
 
269
251
  const app = express();
@@ -271,17 +253,11 @@ app.use(express.json());
271
253
 
272
254
  app.post('/api', async (req, res) => {
273
255
  const { funcId, params } = req.body;
274
- const func = global.ZERO_COM_SERVER_REGISTRY[funcId];
275
-
276
- if (func) {
277
- try {
278
- const result = await execServerFn(func, { req, res }, params);
279
- res.json(result);
280
- } catch (error) {
281
- res.status(500).json({ error: error.message });
282
- }
283
- } else {
284
- res.status(404).json({ error: `Function with id ${funcId} not found.` });
256
+ try {
257
+ const result = await handle(funcId, { req, res }, params);
258
+ res.json(result);
259
+ } catch (error) {
260
+ res.status(500).json({ error: error.message });
285
261
  }
286
262
  });
287
263
 
@@ -304,12 +280,7 @@ module.exports = {
304
280
  path: __dirname + '/dist',
305
281
  },
306
282
  plugins: [
307
- new ZeroComWebpackPlugin({
308
- patterns: {
309
- client: 'src/client/**',
310
- server: 'src/server/api/**',
311
- },
312
- }),
283
+ new ZeroComWebpackPlugin(),
313
284
  ],
314
285
  };
315
286
  ```
@@ -327,12 +298,7 @@ export default {
327
298
  format: 'cjs',
328
299
  },
329
300
  plugins: [
330
- zeroComRollupPlugin({
331
- patterns: {
332
- client: 'src/client/**',
333
- server: 'src/server/api/**',
334
- },
335
- }),
301
+ zeroComRollupPlugin(),
336
302
  ],
337
303
  };
338
304
  ```
package/index.d.ts CHANGED
@@ -1 +1 @@
1
- export * from './lib/index';
1
+ export * from './lib/runtime';
package/index.js CHANGED
@@ -14,4 +14,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./lib/index"), exports);
17
+ __exportStar(require("./lib/runtime"), exports);
package/lib/common.d.ts CHANGED
@@ -1,10 +1,42 @@
1
+ import { CallExpression, Project, SourceFile } from 'ts-morph';
1
2
  export type Options = {
2
3
  development?: boolean;
3
- patterns: {
4
- client: string;
5
- server: string;
6
- };
7
4
  };
8
- export declare const ZERO_COM_CLIENT_SEND = "ZERO_COM_CLIENT_SEND";
5
+ export type ServerFuncInfo = {
6
+ funcId: string;
7
+ filePath: string;
8
+ exportName: string;
9
+ requireContext: boolean;
10
+ };
11
+ export type ServerFuncRegistry = Map<string, Map<string, ServerFuncInfo>>;
12
+ export declare const ZERO_COM_CLIENT_CALL = "ZERO_COM_CLIENT_CALL";
9
13
  export declare const ZERO_COM_SERVER_REGISTRY = "ZERO_COM_SERVER_REGISTRY";
10
- export declare const formatFuncIdName: (funcName: string, path: string, line: number) => string;
14
+ export declare const SERVER_FUNCTION_WRAPPER_NAME = "func";
15
+ export declare const HANDLE_NAME = "handle";
16
+ export declare const CALL_NAME = "call";
17
+ export declare const EXEC_FUNC_NAME = "execFunc";
18
+ export declare const CONTEXT_TYPE_NAME = "context";
19
+ export declare const LIBRARY_NAME = "zero-com";
20
+ export declare const FILE_EXTENSIONS: string[];
21
+ export declare const formatFuncIdName: (funcName: string, filePath: string, line: number) => string;
22
+ export declare const generateCompilationId: () => string;
23
+ export declare const getReplacements: (compilationId: string) => {
24
+ target: string;
25
+ replacement: string;
26
+ }[];
27
+ export declare const isFromLibrary: (callExpr: CallExpression, libraryName: string) => boolean;
28
+ export declare const resolveFilePath: (basePath: string) => string;
29
+ export declare const createProject: () => Project;
30
+ export declare const buildRegistry: (contextDir: string, registry: ServerFuncRegistry) => void;
31
+ export declare const getImportedServerFunctions: (sourceFile: SourceFile, registry: ServerFuncRegistry) => Map<string, ServerFuncInfo>;
32
+ export declare const transformCallSites: (sourceFile: SourceFile, importedFuncs: Map<string, ServerFuncInfo>) => boolean;
33
+ export declare const transformHandleCalls: (sourceFile: SourceFile) => boolean;
34
+ export declare const transformSendCalls: (sourceFile: SourceFile) => boolean;
35
+ export declare const appendRegistryCode: (sourceFile: SourceFile, fileRegistry: Map<string, ServerFuncInfo>) => string;
36
+ export type TransformResult = {
37
+ content: string;
38
+ transformed: boolean;
39
+ };
40
+ export declare const transformSourceFile: (filePath: string, content: string, registry: ServerFuncRegistry) => TransformResult;
41
+ export declare const emitToJs: (filePath: string, content: string) => string;
42
+ export declare const applyReplacements: (code: string, compilationId: string) => string;
package/lib/common.js CHANGED
@@ -1,9 +1,256 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatFuncIdName = exports.ZERO_COM_SERVER_REGISTRY = exports.ZERO_COM_CLIENT_SEND = void 0;
4
- exports.ZERO_COM_CLIENT_SEND = 'ZERO_COM_CLIENT_SEND';
6
+ exports.applyReplacements = exports.emitToJs = exports.transformSourceFile = exports.appendRegistryCode = 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.EXEC_FUNC_NAME = exports.CALL_NAME = exports.HANDLE_NAME = exports.SERVER_FUNCTION_WRAPPER_NAME = exports.ZERO_COM_SERVER_REGISTRY = exports.ZERO_COM_CLIENT_CALL = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const ts_morph_1 = require("ts-morph");
10
+ // Constants
11
+ exports.ZERO_COM_CLIENT_CALL = 'ZERO_COM_CLIENT_CALL';
5
12
  exports.ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY';
6
- const formatFuncIdName = (funcName, path, line) => {
7
- return `${funcName}@${path}:${line}`;
13
+ exports.SERVER_FUNCTION_WRAPPER_NAME = 'func';
14
+ exports.HANDLE_NAME = 'handle';
15
+ exports.CALL_NAME = 'call';
16
+ exports.EXEC_FUNC_NAME = 'execFunc';
17
+ exports.CONTEXT_TYPE_NAME = 'context';
18
+ exports.LIBRARY_NAME = 'zero-com';
19
+ exports.FILE_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs'];
20
+ const formatFuncIdName = (funcName, filePath, line) => {
21
+ return `${funcName}@${filePath}:${line}`;
8
22
  };
9
23
  exports.formatFuncIdName = formatFuncIdName;
24
+ const generateCompilationId = () => String(Math.floor(Math.random() * 1000000));
25
+ exports.generateCompilationId = generateCompilationId;
26
+ const getReplacements = (compilationId) => [
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}` }
29
+ ];
30
+ exports.getReplacements = getReplacements;
31
+ // Utilities
32
+ const isFromLibrary = (callExpr, libraryName) => {
33
+ const symbol = callExpr.getExpression().getSymbol();
34
+ if (!symbol)
35
+ return false;
36
+ for (const decl of symbol.getDeclarations()) {
37
+ const kind = decl.getKind();
38
+ if (kind === ts_morph_1.ts.SyntaxKind.ImportSpecifier || kind === ts_morph_1.ts.SyntaxKind.NamespaceImport) {
39
+ const importDecl = decl.getFirstAncestorByKind(ts_morph_1.ts.SyntaxKind.ImportDeclaration);
40
+ if ((importDecl === null || importDecl === void 0 ? void 0 : importDecl.getModuleSpecifierValue()) === libraryName)
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ };
46
+ exports.isFromLibrary = isFromLibrary;
47
+ const resolveFilePath = (basePath) => {
48
+ for (const ext of exports.FILE_EXTENSIONS) {
49
+ const tryPath = basePath + ext;
50
+ if (fs_1.default.existsSync(tryPath))
51
+ return tryPath;
52
+ }
53
+ for (const ext of exports.FILE_EXTENSIONS.slice(1)) {
54
+ const tryPath = path_1.default.join(basePath, 'index' + ext);
55
+ if (fs_1.default.existsSync(tryPath))
56
+ return tryPath;
57
+ }
58
+ return '';
59
+ };
60
+ exports.resolveFilePath = resolveFilePath;
61
+ const createProject = () => new ts_morph_1.Project({
62
+ compilerOptions: { target: ts_morph_1.ts.ScriptTarget.ES2017, module: ts_morph_1.ts.ModuleKind.ESNext }
63
+ });
64
+ exports.createProject = createProject;
65
+ const getCalleeName = (callExpr) => {
66
+ const expr = callExpr.getExpression();
67
+ const kind = expr.getKind();
68
+ if (kind === ts_morph_1.SyntaxKind.Identifier)
69
+ return expr.getText();
70
+ if (kind === ts_morph_1.SyntaxKind.PropertyAccessExpression)
71
+ return expr.getName();
72
+ return '';
73
+ };
74
+ const getCalleeFullName = (callExpr) => {
75
+ const expr = callExpr.getExpression();
76
+ const kind = expr.getKind();
77
+ if (kind === ts_morph_1.SyntaxKind.Identifier)
78
+ return expr.getText();
79
+ if (kind === ts_morph_1.SyntaxKind.PropertyAccessExpression)
80
+ return expr.getText();
81
+ return '';
82
+ };
83
+ // Registry building
84
+ const buildRegistry = (contextDir, registry) => {
85
+ registry.clear();
86
+ const scanDirectory = (dir) => {
87
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
88
+ const fullPath = path_1.default.join(dir, entry.name);
89
+ if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
90
+ scanDirectory(fullPath);
91
+ }
92
+ else if (entry.isFile() && exports.FILE_EXTENSIONS.slice(1).includes(path_1.default.extname(entry.name))) {
93
+ scanFileForServerFunctions(fullPath, contextDir, registry);
94
+ }
95
+ }
96
+ };
97
+ scanDirectory(contextDir);
98
+ };
99
+ exports.buildRegistry = buildRegistry;
100
+ const scanFileForServerFunctions = (filePath, contextDir, registry) => {
101
+ var _a;
102
+ const sourceFile = (0, exports.createProject)().createSourceFile(filePath, fs_1.default.readFileSync(filePath, 'utf8'), { overwrite: true });
103
+ const fileRegistry = new Map();
104
+ for (const decl of sourceFile.getVariableDeclarations()) {
105
+ if (!decl.isExported())
106
+ continue;
107
+ const initializer = decl.getInitializer();
108
+ if (!initializer || initializer.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
109
+ continue;
110
+ const callExpr = initializer;
111
+ if (!(0, exports.isFromLibrary)(callExpr, exports.LIBRARY_NAME))
112
+ continue;
113
+ const funcExprText = callExpr.getExpression().getText();
114
+ if (funcExprText !== exports.SERVER_FUNCTION_WRAPPER_NAME && !funcExprText.endsWith(`.${exports.SERVER_FUNCTION_WRAPPER_NAME}`))
115
+ continue;
116
+ const args = callExpr.getArguments();
117
+ if (args.length !== 1 || args[0].getKind() !== ts_morph_1.SyntaxKind.ArrowFunction)
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
+ const exportName = decl.getName();
122
+ fileRegistry.set(exportName, {
123
+ funcId: (0, exports.formatFuncIdName)(exportName, path_1.default.relative(contextDir, filePath), decl.getStartLineNumber()),
124
+ filePath,
125
+ exportName,
126
+ requireContext,
127
+ });
128
+ }
129
+ if (fileRegistry.size > 0)
130
+ registry.set(filePath, fileRegistry);
131
+ };
132
+ // Imported functions resolution
133
+ const getImportedServerFunctions = (sourceFile, registry) => {
134
+ var _a;
135
+ const importedFuncs = new Map();
136
+ for (const importDecl of sourceFile.getImportDeclarations()) {
137
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
138
+ if (!moduleSpecifier.startsWith('.'))
139
+ continue;
140
+ const resolvedPath = (0, exports.resolveFilePath)(path_1.default.resolve(path_1.default.dirname(sourceFile.getFilePath()), moduleSpecifier));
141
+ const fileRegistry = registry.get(resolvedPath);
142
+ if (!fileRegistry)
143
+ continue;
144
+ for (const namedImport of importDecl.getNamedImports()) {
145
+ const funcInfo = fileRegistry.get(namedImport.getName());
146
+ if (funcInfo)
147
+ importedFuncs.set(((_a = namedImport.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) || namedImport.getName(), funcInfo);
148
+ }
149
+ const nsImport = importDecl.getNamespaceImport();
150
+ if (nsImport) {
151
+ fileRegistry.forEach((info, name) => importedFuncs.set(`${nsImport.getText()}.${name}`, info));
152
+ }
153
+ }
154
+ return importedFuncs;
155
+ };
156
+ exports.getImportedServerFunctions = getImportedServerFunctions;
157
+ // Transformations
158
+ const transformCallSites = (sourceFile, importedFuncs) => {
159
+ if (importedFuncs.size === 0)
160
+ return false;
161
+ let modified = false;
162
+ sourceFile.forEachDescendant((node) => {
163
+ if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
164
+ return;
165
+ const callExpr = node;
166
+ const calleeName = getCalleeFullName(callExpr);
167
+ const funcInfo = importedFuncs.get(calleeName);
168
+ if (!funcInfo)
169
+ return;
170
+ const args = callExpr.getArguments().map(a => a.getText()).join(', ');
171
+ callExpr.replaceWithText(`globalThis.${exports.ZERO_COM_CLIENT_CALL}('${funcInfo.funcId}', [${args}])`);
172
+ modified = true;
173
+ });
174
+ return modified;
175
+ };
176
+ exports.transformCallSites = transformCallSites;
177
+ const transformHandleCalls = (sourceFile) => {
178
+ let modified = false;
179
+ sourceFile.forEachDescendant((node) => {
180
+ if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
181
+ return;
182
+ const callExpr = node;
183
+ if (!(0, exports.isFromLibrary)(callExpr, exports.LIBRARY_NAME) || getCalleeName(callExpr) !== exports.HANDLE_NAME)
184
+ return;
185
+ const args = callExpr.getArguments();
186
+ if (args.length < 3)
187
+ return;
188
+ callExpr.replaceWithText(`${exports.EXEC_FUNC_NAME}(globalThis.${exports.ZERO_COM_SERVER_REGISTRY}[${args[0].getText()}], ${args[1].getText()}, ${args[2].getText()})`);
189
+ modified = true;
190
+ });
191
+ return modified;
192
+ };
193
+ exports.transformHandleCalls = transformHandleCalls;
194
+ const transformSendCalls = (sourceFile) => {
195
+ let modified = false;
196
+ sourceFile.forEachDescendant((node) => {
197
+ if (node.getKind() !== ts_morph_1.SyntaxKind.CallExpression)
198
+ return;
199
+ const callExpr = node;
200
+ if (!(0, exports.isFromLibrary)(callExpr, exports.LIBRARY_NAME) || getCalleeName(callExpr) !== exports.CALL_NAME)
201
+ return;
202
+ const args = callExpr.getArguments();
203
+ if (args.length < 1)
204
+ return;
205
+ callExpr.replaceWithText(`globalThis.${exports.ZERO_COM_CLIENT_CALL} = ${args[0].getText()}`);
206
+ modified = true;
207
+ });
208
+ return modified;
209
+ };
210
+ exports.transformSendCalls = transformSendCalls;
211
+ const appendRegistryCode = (sourceFile, fileRegistry) => {
212
+ const registrations = Array.from(fileRegistry.values())
213
+ .map(info => `globalThis.${exports.ZERO_COM_SERVER_REGISTRY}['${info.funcId}'] = ${info.exportName}`)
214
+ .join(';\n');
215
+ return `${sourceFile.getFullText()}
216
+ if (!globalThis.${exports.ZERO_COM_SERVER_REGISTRY}) globalThis.${exports.ZERO_COM_SERVER_REGISTRY} = Object.create(null);
217
+ ${registrations};`;
218
+ };
219
+ exports.appendRegistryCode = appendRegistryCode;
220
+ const transformSourceFile = (filePath, content, registry) => {
221
+ const project = (0, exports.createProject)();
222
+ const sourceFile = project.createSourceFile(filePath, content, { overwrite: true });
223
+ const fileRegistry = registry.get(filePath);
224
+ const isServerFunctionFile = fileRegistry && fileRegistry.size > 0;
225
+ const importedFuncs = (0, exports.getImportedServerFunctions)(sourceFile, registry);
226
+ // Don't transform calls to functions defined in the same file
227
+ if (isServerFunctionFile) {
228
+ fileRegistry.forEach((_, name) => importedFuncs.delete(name));
229
+ }
230
+ const callsTransformed = (0, exports.transformCallSites)(sourceFile, importedFuncs);
231
+ const handleTransformed = (0, exports.transformHandleCalls)(sourceFile);
232
+ const sendTransformed = (0, exports.transformSendCalls)(sourceFile);
233
+ if (isServerFunctionFile) {
234
+ return { content: (0, exports.appendRegistryCode)(sourceFile, fileRegistry), transformed: true };
235
+ }
236
+ if (callsTransformed || handleTransformed || sendTransformed) {
237
+ return { content: sourceFile.getFullText(), transformed: true };
238
+ }
239
+ return { content, transformed: false };
240
+ };
241
+ exports.transformSourceFile = transformSourceFile;
242
+ const emitToJs = (filePath, content) => {
243
+ const project = (0, exports.createProject)();
244
+ project.createSourceFile(filePath, content, { overwrite: true });
245
+ const files = project.emitToMemory().getFiles();
246
+ return files.length > 0 ? files[0].text : content;
247
+ };
248
+ exports.emitToJs = emitToJs;
249
+ const applyReplacements = (code, compilationId) => {
250
+ let result = code;
251
+ for (const { target, replacement } of (0, exports.getReplacements)(compilationId)) {
252
+ result = result.replaceAll(target, replacement);
253
+ }
254
+ return result;
255
+ };
256
+ exports.applyReplacements = applyReplacements;
package/lib/rollup.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Plugin } from 'rollup';
2
2
  import { Options } from './common';
3
- export declare function zeroComRollupPlugin(options: Options): Plugin;
3
+ export declare function zeroComRollupPlugin(options?: Options): Plugin;
package/lib/rollup.js CHANGED
@@ -14,158 +14,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.zeroComRollupPlugin = zeroComRollupPlugin;
16
16
  const fs_1 = __importDefault(require("fs"));
17
- const minimatch_1 = require("minimatch");
18
17
  const path_1 = __importDefault(require("path"));
19
- const ts_morph_1 = require("ts-morph");
20
- const typescript_1 = __importDefault(require("typescript"));
21
18
  const common_1 = require("./common");
22
- function zeroComRollupPlugin(options) {
23
- const { development = true, patterns } = options;
24
- const compilationId = String(Math.floor(Math.random() * 1000000));
25
- const clientPattern = patterns.client;
26
- const serverPattern = patterns.server;
27
- const replacements = [
28
- { target: common_1.ZERO_COM_CLIENT_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${compilationId}` },
29
- { target: common_1.ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${compilationId}` }
30
- ];
19
+ function zeroComRollupPlugin(options = {}) {
20
+ const { development = true } = options;
21
+ const compilationId = (0, common_1.generateCompilationId)();
22
+ const registry = new Map();
31
23
  return {
32
24
  name: 'zero-com-rollup-plugin',
33
- resolveId(source, importer, options) {
25
+ buildStart() {
26
+ (0, common_1.buildRegistry)(process.cwd(), registry);
27
+ console.log(`[ZeroComRollupPlugin] Found ${registry.size} files with server functions`);
28
+ },
29
+ resolveId(source, importer, opts) {
34
30
  return __awaiter(this, void 0, void 0, function* () {
35
- if (!importer)
36
- return null;
37
- const resolveResult = yield this.resolve(source, importer, Object.assign(Object.assign({}, options), { skipSelf: true }));
38
- if (!resolveResult)
31
+ if (!importer || (!source.startsWith('.') && !source.startsWith('/')))
39
32
  return null;
40
- const absolutePath = resolveResult.id;
41
- const isServerFile = (0, minimatch_1.minimatch)(absolutePath, path_1.default.join(process.cwd(), serverPattern));
42
- if (!isServerFile)
33
+ const result = yield this.resolve(source, importer, Object.assign(Object.assign({}, opts), { skipSelf: true }));
34
+ if (!result)
43
35
  return null;
44
- const requestedFromClient = (0, minimatch_1.minimatch)(importer, path_1.default.join(process.cwd(), clientPattern));
45
- const tsPath = absolutePath + '.ts';
46
- const jsPath = absolutePath + '.js';
47
- const mjsPath = absolutePath + '.mjs';
48
- let resolvedPath = '';
49
- if (fs_1.default.existsSync(tsPath)) {
50
- resolvedPath = tsPath;
51
- }
52
- else if (fs_1.default.existsSync(jsPath)) {
53
- resolvedPath = jsPath;
54
- }
55
- else if (fs_1.default.existsSync(mjsPath)) {
56
- resolvedPath = mjsPath;
57
- }
58
- else {
36
+ let resolvedPath = (0, common_1.resolveFilePath)(result.id);
37
+ if (!resolvedPath && fs_1.default.existsSync(result.id))
38
+ resolvedPath = result.id;
39
+ if (!resolvedPath)
59
40
  return null;
60
- }
61
- return {
62
- id: resolvedPath,
63
- meta: {
64
- isClient: requestedFromClient,
65
- }
66
- };
41
+ return { id: resolvedPath, meta: { needsTransform: true } };
67
42
  });
68
43
  },
69
44
  load(id) {
70
- var _a;
71
- const meta = (_a = this.getModuleInfo(id)) === null || _a === void 0 ? void 0 : _a.meta;
72
- if (meta === undefined || meta.isClient === undefined)
45
+ var _a, _b;
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))
73
47
  return null;
74
- const originalContent = fs_1.default.readFileSync(id, 'utf8');
75
- const project = new ts_morph_1.Project({
76
- compilerOptions: {
77
- target: typescript_1.default.ScriptTarget.ES2017,
78
- module: typescript_1.default.ModuleKind.ESNext,
79
- },
80
- });
81
- const sourceFile = project.createSourceFile(id, originalContent, { overwrite: true });
82
- if (meta.isClient) {
83
- sourceFile.getFunctions().forEach(func => {
84
- if (func.isExported() && func.isAsync()) {
85
- const funcName = String(func.getName());
86
- const lineNumber = func.getStartLineNumber();
87
- const funcParams = func.getParameters().map(p => p.getName()).join(', ');
88
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
89
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
90
- func.setBodyText(newFunctionBody);
91
- }
92
- });
93
- sourceFile.getVariableDeclarations().forEach(decl => {
94
- const initializer = decl.getInitializer();
95
- if (!initializer || !decl.isExported())
96
- return;
97
- if (initializer instanceof ts_morph_1.ArrowFunction && initializer.isAsync()) {
98
- const funcName = decl.getName();
99
- const lineNumber = decl.getStartLineNumber();
100
- const funcParams = initializer.getParameters().map(p => p.getName()).join(', ');
101
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
102
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
103
- initializer.setBodyText(newFunctionBody);
104
- }
105
- else if (initializer.getKind() === typescript_1.default.SyntaxKind.CallExpression && initializer.getExpression().getText() === 'serverFn') {
106
- const call = initializer;
107
- const arg = call.getArguments()[0];
108
- if (arg && arg instanceof ts_morph_1.ArrowFunction) {
109
- const funcName = decl.getName();
110
- const lineNumber = decl.getStartLineNumber();
111
- const funcParams = arg.getParameters().map(p => p.getName()).join(', ');
112
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
113
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
114
- // Create a new arrow function string
115
- const newArrowFunc = `(${funcParams}) => { ${newFunctionBody} }`;
116
- // Replace the serverFn call with the new arrow function
117
- decl.setInitializer(newArrowFunc);
118
- }
119
- }
120
- });
121
- }
122
- else {
123
- const chunks = [];
124
- sourceFile.getFunctions().forEach(func => {
125
- if (func.isExported() && func.isAsync()) {
126
- const funcName = String(func.getName());
127
- const lineNumber = func.getStartLineNumber();
128
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
129
- chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
130
- }
131
- });
132
- sourceFile.getVariableDeclarations().forEach(decl => {
133
- const initializer = decl.getInitializer();
134
- if (initializer) {
135
- const isServerFn = initializer.getKind() === typescript_1.default.SyntaxKind.CallExpression && initializer.getExpression().getText() === 'serverFn';
136
- if ((initializer instanceof ts_morph_1.ArrowFunction && decl.isExported() && initializer.isAsync()) ||
137
- (isServerFn && decl.isExported())) {
138
- const funcName = decl.getName();
139
- const lineNumber = decl.getStartLineNumber();
140
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
141
- chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
142
- }
143
- }
144
- });
145
- if (chunks.length > 0) {
146
- const textToAdd = `\nif (!global.${common_1.ZERO_COM_SERVER_REGISTRY}) global.${common_1.ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`;
147
- sourceFile.insertText(sourceFile.getEnd(), textToAdd);
148
- }
149
- }
150
- const result = project.emitToMemory();
151
- const newContent = result.getFiles()[0].text;
152
- return newContent;
48
+ const content = fs_1.default.readFileSync(id, 'utf8');
49
+ const result = (0, common_1.transformSourceFile)(id, content, registry);
50
+ if (!result.transformed)
51
+ return null;
52
+ console.log(`[ZeroComRollupPlugin] Transformed: ${path_1.default.relative(process.cwd(), id)}`);
53
+ return (0, common_1.emitToJs)(id, result.content);
153
54
  },
154
- renderChunk(code, chunk, options) {
55
+ renderChunk(code) {
155
56
  if (development)
156
57
  return null;
157
- let modified = false;
158
- let newCode = code;
159
- replacements.forEach(({ target, replacement }) => {
160
- if (newCode.includes(target)) {
161
- newCode = newCode.replaceAll(target, replacement);
162
- modified = true;
163
- }
164
- });
165
- if (modified) {
166
- return { code: newCode, map: null };
167
- }
168
- return null;
58
+ const newCode = (0, common_1.applyReplacements)(code, compilationId);
59
+ return newCode !== code ? { code: newCode, map: null } : null;
169
60
  }
170
61
  };
171
62
  }
@@ -0,0 +1,16 @@
1
+ declare global {
2
+ var ZERO_COM_SERVER_REGISTRY: {
3
+ [funcId: string]: (...args: any[]) => any;
4
+ };
5
+ var ZERO_COM_CLIENT_CALL: (funcId: string, args: any[]) => Promise<any>;
6
+ }
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 execFunc: (sfn: ReturnType<typeof func>, ctx: any, args: any[]) => ReturnType<typeof sfn>;
14
+ export declare const handle: (_funcId: string, _ctx: any, _args: any[]) => any;
15
+ export declare const call: (_fn: (funcId: string, args: any[]) => Promise<any>) => void;
16
+ export {};
package/lib/runtime.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.call = exports.handle = exports.execFunc = void 0;
4
+ exports.func = func;
5
+ // Implementation
6
+ function func(fn) {
7
+ fn.requireContext = true;
8
+ return fn;
9
+ }
10
+ // Internal implementation - receives the actual function from registry
11
+ const execFunc = (sfn, ctx, args) => {
12
+ const fn = sfn;
13
+ if (fn.requireContext) {
14
+ return fn(ctx, ...args);
15
+ }
16
+ else {
17
+ return fn(...args);
18
+ }
19
+ };
20
+ exports.execFunc = execFunc;
21
+ // User-facing function - transformed by plugin to execFunc(globalThis.ZERO_COM_SERVER_REGISTRY[funcId], ctx, args)
22
+ const handle = (_funcId, _ctx, _args) => {
23
+ throw new Error('handle() was not transformed. Ensure the zero-com plugin is configured.');
24
+ };
25
+ exports.handle = handle;
26
+ // User-facing function - transformed by plugin to globalThis.ZERO_COM_CLIENT_CALL = fn
27
+ const call = (_fn) => {
28
+ throw new Error('call() was not transformed. Ensure the zero-com plugin is configured.');
29
+ };
30
+ exports.call = call;
package/lib/webpack.d.ts CHANGED
@@ -1,15 +1,9 @@
1
1
  import { Compiler } from 'webpack';
2
2
  import { Options } from './common';
3
- export type Message = {
4
- funcId: string;
5
- params: any[];
6
- [key: string]: any;
7
- };
8
3
  export declare class ZeroComWebpackPlugin {
9
4
  private options;
10
5
  private compilationId;
11
- private clientPattern;
12
- private serverPattern;
13
- constructor(options: Options);
6
+ private registry;
7
+ constructor(options?: Options);
14
8
  apply(compiler: Compiler): void;
15
9
  }
package/lib/webpack.js CHANGED
@@ -5,157 +5,52 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ZeroComWebpackPlugin = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
- const minimatch_1 = require("minimatch");
9
8
  const path_1 = __importDefault(require("path"));
10
- const ts_morph_1 = require("ts-morph");
11
- const typescript_1 = __importDefault(require("typescript"));
12
9
  const common_1 = require("./common");
13
10
  class ZeroComWebpackPlugin {
14
- constructor(options) {
15
- this.options = Object.assign(Object.assign({ development: true }, options), { patterns: Object.assign({}, options.patterns) });
16
- this.compilationId = String(Math.floor(Math.random() * 1000000));
17
- this.clientPattern = this.options.patterns.client;
18
- this.serverPattern = this.options.patterns.server;
11
+ constructor(options = {}) {
12
+ this.registry = new Map();
13
+ this.options = Object.assign({ development: true }, options);
14
+ this.compilationId = (0, common_1.generateCompilationId)();
19
15
  }
20
16
  apply(compiler) {
21
17
  const pluginName = ZeroComWebpackPlugin.name;
22
18
  const { webpack } = compiler;
23
- const { RawSource } = webpack.sources;
19
+ // Build registry before compilation
20
+ compiler.hooks.beforeCompile.tap(pluginName, () => {
21
+ (0, common_1.buildRegistry)(compiler.context, this.registry);
22
+ console.log(`[ZeroComWebpackPlugin] Found ${this.registry.size} files with server functions`);
23
+ });
24
+ // Transform files during module resolution
24
25
  compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
25
26
  nmf.hooks.beforeResolve.tap(pluginName, (resolveData) => {
26
- const absolutePath = path_1.default.resolve(resolveData.context, resolveData.request);
27
- const isServerFile = (0, minimatch_1.minimatch)(absolutePath, path_1.default.join(compiler.context, this.serverPattern));
28
- if (!isServerFile)
27
+ if (!resolveData.request.startsWith('.') && !resolveData.request.startsWith('/'))
29
28
  return;
30
- const requestedFromClient = (0, minimatch_1.minimatch)(resolveData.contextInfo.issuer, path_1.default.join(compiler.context, this.clientPattern));
31
- const tsPath = absolutePath + '.ts';
32
- const jsPath = absolutePath + '.js';
33
- const mjsPath = absolutePath + '.mjs';
34
- let resolvedPath = '';
35
- if (fs_1.default.existsSync(tsPath)) {
36
- resolvedPath = tsPath;
37
- }
38
- else if (fs_1.default.existsSync(jsPath)) {
39
- resolvedPath = jsPath;
40
- }
41
- else if (fs_1.default.existsSync(mjsPath)) {
42
- resolvedPath = mjsPath;
43
- }
44
- else {
45
- throw new Error('Unable to resolve: ' + absolutePath);
46
- }
47
- const originalContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
48
- const project = new ts_morph_1.Project({
49
- compilerOptions: {
50
- target: typescript_1.default.ScriptTarget.ES2017,
51
- module: typescript_1.default.ModuleKind.ESNext,
52
- },
53
- });
54
- const sourceFile = project.createSourceFile(absolutePath, originalContent, { overwrite: true });
55
- let newModuleContent = '';
56
- if (requestedFromClient) {
57
- const generatedFunctions = [];
58
- sourceFile.getFunctions().forEach(func => {
59
- if (func.isExported() && func.isAsync()) {
60
- const funcName = String(func.getName());
61
- const lineNumber = func.getStartLineNumber();
62
- const funcParams = func.getParameters().map(p => p.getName()).join(', ');
63
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
64
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
65
- func.setBodyText(newFunctionBody);
66
- generatedFunctions.push(func.getText());
67
- console.log('client:', funcId);
68
- }
69
- });
70
- sourceFile.getVariableDeclarations().forEach(decl => {
71
- const initializer = decl.getInitializer();
72
- if (!initializer || !decl.isExported())
73
- return;
74
- if (initializer instanceof ts_morph_1.ArrowFunction && initializer.isAsync()) {
75
- const funcName = decl.getName();
76
- const lineNumber = decl.getStartLineNumber();
77
- const funcParams = initializer.getParameters().map(p => p.getName()).join(', ');
78
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
79
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
80
- initializer.setBodyText(newFunctionBody);
81
- generatedFunctions.push(decl.getVariableStatementOrThrow().getText());
82
- console.log('client:', funcId);
83
- }
84
- else if (initializer.getKind() === typescript_1.default.SyntaxKind.CallExpression && initializer.getExpression().getText() === 'serverFn') {
85
- const call = initializer;
86
- const arg = call.getArguments()[0];
87
- if (arg && arg instanceof ts_morph_1.ArrowFunction) {
88
- const funcName = decl.getName();
89
- const lineNumber = decl.getStartLineNumber();
90
- const funcParams = arg.getParameters().map(p => p.getName()).join(', ');
91
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
92
- const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
93
- // Create a new arrow function string
94
- const newArrowFunc = `(${funcParams}) => { ${newFunctionBody} }`;
95
- // Replace the serverFn call with the new arrow function
96
- decl.setInitializer(newArrowFunc);
97
- generatedFunctions.push(decl.getVariableStatementOrThrow().getText());
98
- console.log('client:', funcId);
99
- }
100
- }
101
- });
102
- newModuleContent = generatedFunctions.join('\n\n');
103
- }
104
- else {
105
- const chunks = [];
106
- sourceFile.getFunctions().forEach(func => {
107
- if (func.isExported() && func.isAsync()) {
108
- const funcName = String(func.getName());
109
- const lineNumber = func.getStartLineNumber();
110
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
111
- chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
112
- console.log('server:', funcId);
113
- }
114
- });
115
- sourceFile.getVariableDeclarations().forEach(decl => {
116
- const initializer = decl.getInitializer();
117
- if (initializer) {
118
- const isServerFn = initializer.getKind() === typescript_1.default.SyntaxKind.CallExpression && initializer.getExpression().getText() === 'serverFn';
119
- if ((initializer instanceof ts_morph_1.ArrowFunction && decl.isExported() && initializer.isAsync()) ||
120
- (isServerFn && decl.isExported())) {
121
- const funcName = decl.getName();
122
- const lineNumber = decl.getStartLineNumber();
123
- const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
124
- chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
125
- console.log('server:', funcId);
126
- }
127
- }
128
- });
129
- newModuleContent = `${originalContent} if (!global.${common_1.ZERO_COM_SERVER_REGISTRY}) global.${common_1.ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`;
130
- }
131
- project.createSourceFile(absolutePath + '.ts', newModuleContent, { overwrite: true });
132
- const result = project.emitToMemory();
133
- const newContent = result.getFiles()[0].text;
134
- const inlineLoader = `data:text/javascript,${encodeURIComponent(newContent)}`;
135
- resolveData.request = inlineLoader;
29
+ const resolvedPath = (0, common_1.resolveFilePath)(path_1.default.resolve(resolveData.context, resolveData.request));
30
+ if (!resolvedPath || !fs_1.default.existsSync(resolvedPath))
31
+ return;
32
+ const content = fs_1.default.readFileSync(resolvedPath, 'utf8');
33
+ const result = (0, common_1.transformSourceFile)(resolvedPath, content, this.registry);
34
+ if (!result.transformed)
35
+ return;
36
+ const jsContent = (0, common_1.emitToJs)(resolvedPath, result.content);
37
+ resolveData.request = `data:text/javascript,${encodeURIComponent(jsContent)}`;
38
+ console.log(`[ZeroComWebpackPlugin] Transformed: ${path_1.default.relative(compiler.context, resolvedPath)}`);
136
39
  });
137
40
  });
41
+ // Production: minify global names
138
42
  if (this.options.development)
139
43
  return;
140
- const replacements = [
141
- { target: common_1.ZERO_COM_CLIENT_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${this.compilationId}` },
142
- { target: common_1.ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${this.compilationId}` }
143
- ];
44
+ const { RawSource } = webpack.sources;
144
45
  compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
145
46
  compilation.hooks.processAssets.tap({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE }, (assets) => {
146
47
  for (const assetName in assets) {
147
- if (assetName.endsWith('.js')) {
148
- let assetSource = String(assets[assetName].source());
149
- let modified = false;
150
- replacements.forEach(({ target, replacement }) => {
151
- if (assetSource.includes(target)) {
152
- assetSource = assetSource.replaceAll(target, replacement);
153
- modified = true;
154
- }
155
- });
156
- if (modified) {
157
- compilation.updateAsset(assetName, new RawSource(assetSource));
158
- }
48
+ if (!assetName.endsWith('.js'))
49
+ continue;
50
+ const source = String(assets[assetName].source());
51
+ const newSource = (0, common_1.applyReplacements)(source, this.compilationId);
52
+ if (newSource !== source) {
53
+ compilation.updateAsset(assetName, new RawSource(newSource));
159
54
  }
160
55
  }
161
56
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-com",
3
- "version": "0.0.8",
3
+ "version": "1.2.0",
4
4
  "main": "index.js",
5
5
  "repository": "https://github.com/yosbelms/zero-com",
6
6
  "keywords": [
@@ -13,6 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "npx tsc -d",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
16
18
  "clean": "find . -type f \\( -name \"*.js\" -o -name \"*.js.map\" -o -name \"*.d.ts\" -o -name \"*.d.ts.map\" \\) | grep -v \"./node_modules\" | xargs rm",
17
19
  "prepublishOnly": "npm run clean && npm run build"
18
20
  },
@@ -26,7 +28,8 @@
26
28
  "devDependencies": {
27
29
  "@types/webpack": "^5.28.5",
28
30
  "rollup": "^4.52.5",
29
- "typescript": "^5.8.3"
31
+ "typescript": "^5.8.3",
32
+ "vitest": "^4.0.18"
30
33
  },
31
34
  "peerDependencies": {
32
35
  "typescript": "^5.0.0"
package/tsconfig.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  /* Visit https://aka.ms/tsconfig to read more about this file */
4
-
5
4
  /* Projects */
6
5
  // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7
6
  // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
@@ -9,10 +8,11 @@
9
8
  // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10
9
  // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11
10
  // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12
-
13
11
  /* Language and Environment */
14
- "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15
- "lib": ["es2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
12
+ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
13
+ "lib": [
14
+ "es2021"
15
+ ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16
16
  // "jsx": "preserve", /* Specify what JSX code is generated. */
17
17
  // "libReplacement": true, /* Enable lib replacement. */
18
18
  // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
@@ -24,9 +24,8 @@
24
24
  // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
25
25
  // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
26
26
  // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
27
-
28
27
  /* Modules */
29
- "module": "commonjs", /* Specify what module code is generated. */
28
+ "module": "commonjs", /* Specify what module code is generated. */
30
29
  // "rootDir": "./", /* Specify the root folder within your source files. */
31
30
  // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
32
31
  // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
@@ -42,15 +41,13 @@
42
41
  // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
43
42
  // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
44
43
  // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
45
- // "resolveJsonModule": true, /* Enable importing .json files. */
44
+ "resolveJsonModule": true, /* Enable importing .json files. */
46
45
  // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
47
46
  // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
48
-
49
47
  /* JavaScript Support */
50
48
  // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
51
49
  // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
52
50
  // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
53
-
54
51
  /* Emit */
55
52
  // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
56
53
  // "declarationMap": true, /* Create sourcemaps for d.ts files. */
@@ -73,19 +70,17 @@
73
70
  // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
74
71
  // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
75
72
  // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
76
-
77
73
  /* Interop Constraints */
78
74
  // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
79
75
  // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
80
76
  // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
81
77
  // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
82
78
  // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
83
- "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
79
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
84
80
  // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
85
- "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
86
-
81
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
87
82
  /* Type Checking */
88
- "strict": true, /* Enable all strict type-checking options. */
83
+ "strict": true, /* Enable all strict type-checking options. */
89
84
  // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
90
85
  // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
91
86
  // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
@@ -105,9 +100,13 @@
105
100
  // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
106
101
  // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
107
102
  // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
108
-
109
103
  /* Completeness */
110
104
  // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
111
- "skipLibCheck": true /* Skip type checking all .d.ts files. */
105
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
106
+ "paths": {
107
+ "zero-com": [
108
+ "./index.ts"
109
+ ]
110
+ }
112
111
  }
113
- }
112
+ }
package/lib/index.d.ts DELETED
@@ -1,11 +0,0 @@
1
- declare global {
2
- var ZERO_COM_SERVER_REGISTRY: {
3
- [funcId: string]: (...args: any[]) => any;
4
- };
5
- var ZERO_COM_CLIENT_SEND: (...args: any[]) => Promise<any>;
6
- }
7
- export declare const serverFn: <Ctx, Rest extends any[], R>(sfn: (ctx: Ctx, ...rest: Rest) => R) => {
8
- (...rest: Rest): R;
9
- serverFn: (ctx: Ctx, ...rest: Rest) => R;
10
- };
11
- export declare const execServerFn: (sfn: ReturnType<typeof serverFn>, ctx: any, args: any[]) => ReturnType<typeof sfn>;
package/lib/index.js DELETED
@@ -1,18 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.execServerFn = exports.serverFn = void 0;
4
- const serverFn = (sfn) => {
5
- const clonedSfn = (...rest) => sfn(null, ...rest);
6
- clonedSfn.serverFn = sfn;
7
- return clonedSfn;
8
- };
9
- exports.serverFn = serverFn;
10
- const execServerFn = (sfn, ctx, args) => {
11
- if (sfn.serverFn) {
12
- return sfn.serverFn.call(null, ctx, ...args);
13
- }
14
- else {
15
- return sfn.call(null, ...args);
16
- }
17
- };
18
- exports.execServerFn = execServerFn;