zero-com 0.0.7-s → 1.0.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
  ```
@@ -84,23 +77,25 @@ Zero-com does not define any transport layer, it is up to you to create one or r
84
77
  The following diagram illustrates the communication flow between the client and server:
85
78
 
86
79
  ```
87
- +--------+ +-----------------------------+ +-------------+
88
- | Client |----->| window.ZERO_COM_CLIENT_SEND |----->| Your Server |
89
- +--------+ +-----------------------------+ +-------------+
90
- |
91
- v
92
- +--------+ +-------------------------+ +-------------------+
93
- | Client |<-----| (Your custom transport) |<-----| someCustomHandler |
94
- +--------+ +-------------------------+ +-------------------+
80
+ +--------+ +----------------------------------+ +-------------+
81
+ | Client |----->| globalThis.ZERO_COM_CLIENT_SEND |----->| Your Server |
82
+ +--------+ +----------------------------------+ +-------------+
83
+ |
84
+ v
85
+ +--------+ +-------------------------+ +--------------------------------------+
86
+ | Client |<-----| (Your custom transport) |<-----| globalThis.ZERO_COM_SERVER_REGISTRY |
87
+ +--------+ +-------------------------+ +--------------------------------------+
95
88
  ```
96
89
 
97
90
  ### Client-side
98
91
 
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.
92
+ All messages from the client-side will be sent using the transport function you define. Import `send` from `zero-com` and pass your transport function.
100
93
 
101
94
  ```javascript
102
95
  // client/transport.js
103
- window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
96
+ import { send } from 'zero-com';
97
+
98
+ send(async (funcId, params) => {
104
99
  const response = await fetch('http://localhost:8000/api', {
105
100
  method: 'POST',
106
101
  headers: {
@@ -109,24 +104,19 @@ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
109
104
  body: JSON.stringify({ funcId, params }),
110
105
  });
111
106
  return await response.json();
112
- };
107
+ });
113
108
  ```
114
109
 
115
110
  ### Server-side
116
111
 
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.
112
+ 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
113
 
119
114
  ```javascript
120
115
  // server/api.js
121
- import { execServerFn } from 'zero-com';
116
+ import { handle } from 'zero-com';
122
117
 
123
118
  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
- }
119
+ return await handle(message.funcId, null, message.params);
130
120
  };
131
121
 
132
122
  // Example of how to use the handler with an Express server
@@ -155,13 +145,17 @@ Often you want to pass a context-related object to the server functions to have
155
145
 
156
146
  ### Passing Context to Server Functions
157
147
 
158
- To pass context to a server function, you need to wrap the function in `serverFn` and receive the context as the first parameter.
148
+ 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
149
 
160
- ```javascript
161
- // server/api/phones.js
162
- import { serverFn } from 'zero-com';
150
+ ```typescript
151
+ // server/api/phones.ts
152
+ import { func, context } from 'zero-com';
153
+
154
+ type MyContext = {
155
+ request: any
156
+ }
163
157
 
164
- export const getPhones = serverFn(async (ctx, name) => {
158
+ export const getPhones = func(async (ctx: context<MyContext>, name: string) => {
165
159
  // ctx is the context object passed from the server
166
160
  console.log(ctx.request.headers);
167
161
  // ... your code
@@ -170,17 +164,16 @@ export const getPhones = serverFn(async (ctx, name) => {
170
164
 
171
165
  ### Providing Context on the Server
172
166
 
173
- You can pass the context to `execServerFn` when you execute the server function.
167
+ You can pass the context to `handle` when you execute the server function.
174
168
 
175
169
  ```javascript
176
170
  // server/api.js
177
- import { execServerFn } from 'zero-com';
171
+ import { handle } from 'zero-com';
178
172
 
179
173
  const myHandler = (request, response, message) => {
180
174
  const ctx = { request, response };
181
- const func = global.ZERO_COM_SERVER_REGISTRY[message.funcId];
182
175
  // pass context on exec
183
- return execServerFn(func, ctx, message.params);
176
+ return handle(message.funcId, ctx, message.params);
184
177
  };
185
178
  ```
186
179
 
@@ -189,9 +182,6 @@ const myHandler = (request, response, message) => {
189
182
  | Option | Type | Description |
190
183
  |-------------|-----------|-----------------------------------------------------------------------------|
191
184
  | `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
185
 
196
186
  ## Complete Example
197
187
 
@@ -206,17 +196,17 @@ Here's a complete example of how to use Zero-com in a project.
206
196
  ├── rollup.config.js
207
197
  └── src
208
198
  ├── client
209
- │ ├── index.js
210
- │ └── transport.js
199
+ │ ├── index.ts
200
+ │ └── transport.ts
211
201
  └── server
212
202
  └── api
213
- └── phones.js
203
+ └── phones.ts
214
204
  ```
215
205
 
216
206
  ### Client-side
217
207
 
218
- ```javascript
219
- // src/client/index.js
208
+ ```typescript
209
+ // src/client/index.ts
220
210
  import { getPhones } from '../../server/api/phones';
221
211
 
222
212
  async function main() {
@@ -227,9 +217,11 @@ async function main() {
227
217
  main();
228
218
  ```
229
219
 
230
- ```javascript
231
- // src/client/transport.js
232
- window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
220
+ ```typescript
221
+ // src/client/transport.ts
222
+ import { send } from 'zero-com';
223
+
224
+ send(async (funcId, params) => {
233
225
  const response = await fetch('http://localhost:8000/api', {
234
226
  method: 'POST',
235
227
  headers: {
@@ -238,16 +230,21 @@ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
238
230
  body: JSON.stringify({ funcId, params }),
239
231
  });
240
232
  return await response.json();
241
- };
233
+ });
242
234
  ```
243
235
 
244
236
  ### Server-side
245
237
 
246
- ```javascript
247
- // src/server/api/phones.js
248
- import { serverFn } from 'zero-com';
238
+ ```typescript
239
+ // src/server/api/phones.ts
240
+ import { func, context } from 'zero-com';
241
+
242
+ type Context = {
243
+ req: any,
244
+ res: any
245
+ }
249
246
 
250
- export const getPhones = serverFn(async (ctx, name) => {
247
+ export const getPhones = func(async (ctx: context<Context>, name: string) => {
251
248
  // In a real application, you would fetch this from a database
252
249
  const allPhones = [
253
250
  { name: 'iPhone 13', brand: 'Apple' },
@@ -260,10 +257,10 @@ export const getPhones = serverFn(async (ctx, name) => {
260
257
 
261
258
  ### Server
262
259
 
263
- ```javascript
264
- // server.js
260
+ ```typescript
261
+ // server.ts
265
262
  import express from 'express';
266
- import { execServerFn } from 'zero-com';
263
+ import { handle } from 'zero-com';
267
264
  import './src/server/api/phones.js'; // Make sure to import the server-side modules
268
265
 
269
266
  const app = express();
@@ -271,17 +268,11 @@ app.use(express.json());
271
268
 
272
269
  app.post('/api', async (req, res) => {
273
270
  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.` });
271
+ try {
272
+ const result = await handle(funcId, { req, res }, params);
273
+ res.json(result);
274
+ } catch (error) {
275
+ res.status(500).json({ error: error.message });
285
276
  }
286
277
  });
287
278
 
@@ -304,12 +295,7 @@ module.exports = {
304
295
  path: __dirname + '/dist',
305
296
  },
306
297
  plugins: [
307
- new ZeroComWebpackPlugin({
308
- patterns: {
309
- client: 'src/client/**',
310
- server: 'src/server/api/**',
311
- },
312
- }),
298
+ new ZeroComWebpackPlugin(),
313
299
  ],
314
300
  };
315
301
  ```
@@ -327,12 +313,7 @@ export default {
327
313
  format: 'cjs',
328
314
  },
329
315
  plugins: [
330
- zeroComRollupPlugin({
331
- patterns: {
332
- client: 'src/client/**',
333
- server: 'src/server/api/**',
334
- },
335
- }),
316
+ zeroComRollupPlugin(),
336
317
  ],
337
318
  };
338
319
  ```
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
  };
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>>;
8
12
  export declare const ZERO_COM_CLIENT_SEND = "ZERO_COM_CLIENT_SEND";
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 SEND_NAME = "send";
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;
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.SEND_NAME = exports.HANDLE_NAME = exports.SERVER_FUNCTION_WRAPPER_NAME = exports.ZERO_COM_SERVER_REGISTRY = exports.ZERO_COM_CLIENT_SEND = 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
4
11
  exports.ZERO_COM_CLIENT_SEND = 'ZERO_COM_CLIENT_SEND';
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.SEND_NAME = 'send';
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_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${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_SEND}('${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.SEND_NAME)
201
+ return;
202
+ const args = callExpr.getArguments();
203
+ if (args.length < 1)
204
+ return;
205
+ callExpr.replaceWithText(`globalThis.${exports.ZERO_COM_CLIENT_SEND} = ${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_SEND: (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 send: (_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.send = 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_SEND = fn
27
+ const send = (_fn) => {
28
+ throw new Error('send() was not transformed. Ensure the zero-com plugin is configured.');
29
+ };
30
+ exports.send = send;
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.7s",
3
+ "version": "1.0.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;