zero-com 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,28 +2,70 @@
2
2
 
3
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.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Usage](#usage)
8
+ - [Transport layer](#transport-layer)
9
+ - [Context](#context)
10
+ - [Plugin options](#plugin-options)
11
+ - [Complete Example](#complete-example)
12
+
5
13
  ## Usage
6
14
 
7
- Webpack config.
8
- ```js
9
- new ZeroComWebpackPlugin({
10
- development: true,
11
- patterns: {
12
- client: 'src/client/**',
13
- server: 'src/server/api/**',
14
- }
15
- })
15
+ Zero-com can be used with either Webpack or Rollup.
16
+
17
+ ### Webpack
18
+
19
+ To use Zero-com with Webpack, you need to add the `ZeroComWebpackPlugin` to your `webpack.config.js` file.
20
+
21
+ ```javascript
22
+ // webpack.config.js
23
+ const { ZeroComWebpackPlugin } = require('zero-com/webpack');
24
+
25
+ module.exports = {
26
+ // ... your webpack config
27
+ plugins: [
28
+ new ZeroComWebpackPlugin({
29
+ development: true,
30
+ patterns: {
31
+ client: 'src/client/**',
32
+ server: 'src/server/api/**',
33
+ },
34
+ }),
35
+ ],
36
+ };
16
37
  ```
17
38
 
18
- 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 (not arrow functions). See the example below.
39
+ ### Rollup
40
+
41
+ To use Zero-com with Rollup, you need to add the `zeroComRollupPlugin` to your `rollup.config.js` file.
42
+
43
+ ```javascript
44
+ // rollup.config.js
45
+ import zeroComRollupPlugin from 'zero-com/rollup';
46
+
47
+ export default {
48
+ // ... your rollup config
49
+ plugins: [
50
+ zeroComRollupPlugin({
51
+ development: true,
52
+ patterns: {
53
+ client: 'src/client/**',
54
+ server: 'src/server/api/**',
55
+ },
56
+ }),
57
+ ],
58
+ };
59
+ ```
60
+
61
+ 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.
19
62
 
20
63
  Server side
21
64
  ```js
22
65
  // server/phones.ts
23
- // callable
24
66
  export async function getPhones() { }
25
67
 
26
- // not callable from client-side
68
+ // or
27
69
  export const getPhones = async () => { }
28
70
  ```
29
71
 
@@ -33,30 +75,264 @@ Client side
33
75
  import { getPhones } '../server/phones'
34
76
  ```
35
77
 
36
- ## Trasport layer
37
- Zero-com does not define any transport layer, it allows you to define a new one or reuse your own.
78
+ ## Transport layer
38
79
 
39
- - `window.ZERO_COM_CLIENT_SEND` all mesages from client-side will be sent using this function.
40
- - `global.ZERO_COM_SERVER_REGISTRY` object available on the server-side of which the keys are the name of the methods and the values ​​are the functions to be executed.
80
+ 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.
41
81
 
42
- Client side example.
43
- ```js
44
- window.ZERO_COM_CLIENT_SEND = async ({ method, params }) {
45
- // -> send the message to server
46
- // <- return response
47
- }
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
+ +--------+ +-------------------------+ +-------------------+
48
95
  ```
49
96
 
50
- Server side example.
51
- ```js
52
- const someCustomHandler = (message) => {
53
- const func = global.ZERO_COM_SERVER_REGISTRY[message.method]
54
- return func(...message.params)
55
- }
97
+ ### Client-side
98
+
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.
100
+
101
+ ```javascript
102
+ // client/transport.js
103
+ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
104
+ const response = await fetch('http://localhost:8000/api', {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Content-Type': 'application/json',
108
+ },
109
+ body: JSON.stringify({ funcId, params }),
110
+ });
111
+ return await response.json();
112
+ };
113
+ ```
114
+
115
+ ### Server-side
116
+
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.
118
+
119
+ ```javascript
120
+ // server/api.js
121
+ import { execServerFn } from 'zero-com';
122
+
123
+ 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
+ }
130
+ };
131
+
132
+ // Example of how to use the handler with an Express server
133
+ import express from 'express';
134
+
135
+ const app = express();
136
+ app.use(express.json());
137
+
138
+ app.post('/api', async (req, res) => {
139
+ try {
140
+ const result = await someCustomHandler(req.body);
141
+ res.json(result);
142
+ } catch (error) {
143
+ res.status(500).json({ error: error.message });
144
+ }
145
+ });
146
+
147
+ app.listen(8000, () => {
148
+ console.log('Server running at http://localhost:8000/');
149
+ });
150
+ ```
151
+
152
+ ## Context
153
+
154
+ Often you want to pass a context-related object to the server functions to have access to data like the request, response, session, etc. Zero-com provides a simple way to do this.
155
+
156
+ ### Passing Context to Server Functions
157
+
158
+ To pass context to a server function, you need to wrap the function in `serverFn` and receive the context as the first parameter.
159
+
160
+ ```javascript
161
+ // server/api/phones.js
162
+ import { serverFn } from 'zero-com';
163
+
164
+ export const getPhones = serverFn(async (ctx, name) => {
165
+ // ctx is the context object passed from the server
166
+ console.log(ctx.request.headers);
167
+ // ... your code
168
+ });
169
+ ```
170
+
171
+ ### Providing Context on the Server
172
+
173
+ You can pass the context to `execServerFn` when you execute the server function.
174
+
175
+ ```javascript
176
+ // server/api.js
177
+ import { execServerFn } from 'zero-com';
178
+
179
+ const myHandler = (request, response, message) => {
180
+ const ctx = { request, response };
181
+ const func = global.ZERO_COM_SERVER_REGISTRY[message.funcId];
182
+ // pass context on exec
183
+ return execServerFn(func, ctx, message.params);
184
+ };
56
185
  ```
57
186
 
58
187
  ## Plugin options
59
- - development: if `false` will add internal variable renaming to the final bundle.
60
- - patterns
61
- - client: pattern to identify client-side files
62
- - server: pattern to identify server-side files
188
+
189
+ | Option | Type | Description |
190
+ |-------------|-----------|-----------------------------------------------------------------------------|
191
+ | `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
+
196
+ ## Complete Example
197
+
198
+ Here's a complete example of how to use Zero-com in a project.
199
+
200
+ ### Project Structure
201
+
202
+ ```
203
+ .
204
+ ├── package.json
205
+ ├── webpack.config.js
206
+ ├── rollup.config.js
207
+ └── src
208
+ ├── client
209
+ │ ├── index.js
210
+ │ └── transport.js
211
+ └── server
212
+ └── api
213
+ └── phones.js
214
+ ```
215
+
216
+ ### Client-side
217
+
218
+ ```javascript
219
+ // src/client/index.js
220
+ import { getPhones } from '../../server/api/phones';
221
+
222
+ async function main() {
223
+ const phones = await getPhones('iPhone');
224
+ console.log(phones);
225
+ }
226
+
227
+ main();
228
+ ```
229
+
230
+ ```javascript
231
+ // src/client/transport.js
232
+ window.ZERO_COM_CLIENT_SEND = async ({ funcId, params }) => {
233
+ const response = await fetch('http://localhost:8000/api', {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ },
238
+ body: JSON.stringify({ funcId, params }),
239
+ });
240
+ return await response.json();
241
+ };
242
+ ```
243
+
244
+ ### Server-side
245
+
246
+ ```javascript
247
+ // src/server/api/phones.js
248
+ import { serverFn } from 'zero-com';
249
+
250
+ export const getPhones = serverFn(async (ctx, name) => {
251
+ // In a real application, you would fetch this from a database
252
+ const allPhones = [
253
+ { name: 'iPhone 13', brand: 'Apple' },
254
+ { name: 'Galaxy S22', brand: 'Samsung' },
255
+ ];
256
+
257
+ return allPhones.filter((phone) => phone.name.includes(name));
258
+ });
259
+ ```
260
+
261
+ ### Server
262
+
263
+ ```javascript
264
+ // server.js
265
+ import express from 'express';
266
+ import { execServerFn } from 'zero-com';
267
+ import './src/server/api/phones.js'; // Make sure to import the server-side modules
268
+
269
+ const app = express();
270
+ app.use(express.json());
271
+
272
+ app.post('/api', async (req, res) => {
273
+ 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.` });
285
+ }
286
+ });
287
+
288
+ app.listen(8000, () => {
289
+ console.log('Server running at http://localhost:8000/');
290
+ });
291
+ ```
292
+
293
+ ### Webpack Configuration
294
+
295
+ ```javascript
296
+ // webpack.config.js
297
+ const { ZeroComWebpackPlugin } = require('zero-com/webpack');
298
+
299
+ module.exports = {
300
+ mode: 'development',
301
+ entry: './src/client/index.js',
302
+ output: {
303
+ filename: 'main.js',
304
+ path: __dirname + '/dist',
305
+ },
306
+ plugins: [
307
+ new ZeroComWebpackPlugin({
308
+ patterns: {
309
+ client: 'src/client/**',
310
+ server: 'src/server/api/**',
311
+ },
312
+ }),
313
+ ],
314
+ };
315
+ ```
316
+
317
+ ### Rollup Configuration
318
+
319
+ ```javascript
320
+ // rollup.config.js
321
+ import zeroComRollupPlugin from 'zero-com/rollup';
322
+
323
+ export default {
324
+ input: 'src/client/index.js',
325
+ output: {
326
+ file: 'dist/main.js',
327
+ format: 'cjs',
328
+ },
329
+ plugins: [
330
+ zeroComRollupPlugin({
331
+ patterns: {
332
+ client: 'src/client/**',
333
+ server: 'src/server/api/**',
334
+ },
335
+ }),
336
+ ],
337
+ };
338
+ ```
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/index';
package/index.js CHANGED
@@ -1,123 +1,17 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ZeroComWebpackPlugin = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- const minimatch_1 = require("minimatch");
9
- const path_1 = __importDefault(require("path"));
10
- const ts_morph_1 = require("ts-morph");
11
- const typescript_1 = __importDefault(require("typescript"));
12
- class ZeroComWebpackPlugin {
13
- constructor(options) {
14
- this.options = Object.assign(Object.assign({ development: true }, options), { patterns: Object.assign({}, options.patterns) });
15
- this.compilationId = String(Math.floor(Math.random() * 1000000));
16
- this.clientPattern = this.options.patterns.client;
17
- this.serverPattern = this.options.patterns.server;
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
7
  }
19
- apply(compiler) {
20
- const ZERO_COM_CLIENT_SEND = 'ZERO_COM_CLIENT_SEND';
21
- const ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY';
22
- const pluginName = ZeroComWebpackPlugin.name;
23
- const { webpack } = compiler;
24
- const { RawSource } = webpack.sources;
25
- compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
26
- nmf.hooks.beforeResolve.tap(pluginName, (resolveData) => {
27
- const absolutePath = path_1.default.resolve(resolveData.context, resolveData.request);
28
- const isServerFile = (0, minimatch_1.minimatch)(absolutePath, path_1.default.join(compiler.context, this.serverPattern));
29
- if (!isServerFile)
30
- return;
31
- const requestedFromClient = (0, minimatch_1.minimatch)(resolveData.contextInfo.issuer, path_1.default.join(compiler.context, this.clientPattern));
32
- const tsPath = absolutePath + '.ts';
33
- const jsPath = absolutePath + '.js';
34
- const mjsPath = absolutePath + '.mjs';
35
- let resolvedPath = '';
36
- if (fs_1.default.existsSync(tsPath)) {
37
- resolvedPath = tsPath;
38
- }
39
- else if (fs_1.default.existsSync(jsPath)) {
40
- resolvedPath = jsPath;
41
- }
42
- else if (fs_1.default.existsSync(mjsPath)) {
43
- resolvedPath = mjsPath;
44
- }
45
- else {
46
- throw new Error('Unable to resolve: ' + absolutePath);
47
- }
48
- const originalContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
49
- const project = new ts_morph_1.Project({
50
- compilerOptions: {
51
- target: typescript_1.default.ScriptTarget.ES2017,
52
- module: typescript_1.default.ModuleKind.ESNext,
53
- },
54
- });
55
- const sourceFile = project.createSourceFile(absolutePath, originalContent, { overwrite: true });
56
- let newModuleContent = '';
57
- if (requestedFromClient) {
58
- const generatedFunctions = [];
59
- sourceFile.getFunctions().forEach(func => {
60
- if (func.isExported() && func.isAsync()) {
61
- const funcName = String(func.getName());
62
- const lineNumber = func.getStartLineNumber();
63
- const funcParams = func.getParameters().map(p => p.getName()).join(', ');
64
- const method = formatMethodName(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
65
- const newFunctionBody = `return window.${ZERO_COM_CLIENT_SEND}({method: '${method}', params: [${funcParams}]})`;
66
- func.setBodyText(newFunctionBody);
67
- generatedFunctions.push(func.getText());
68
- console.log('client:', method);
69
- }
70
- });
71
- newModuleContent = generatedFunctions.join('\n\n');
72
- }
73
- else {
74
- const chunks = [];
75
- sourceFile.getFunctions().forEach(func => {
76
- if (func.isExported() && func.isAsync()) {
77
- const funcName = String(func.getName());
78
- const lineNumber = func.getStartLineNumber();
79
- const method = formatMethodName(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
80
- chunks.push(`global.${ZERO_COM_SERVER_REGISTRY}['${method}'] = ${funcName}`);
81
- console.log('server:', method);
82
- }
83
- });
84
- newModuleContent = `${originalContent} if (!global.${ZERO_COM_SERVER_REGISTRY}) global.${ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`;
85
- }
86
- project.createSourceFile(absolutePath + '.ts', newModuleContent, { overwrite: true });
87
- const result = project.emitToMemory();
88
- const newContent = result.getFiles()[0].text;
89
- const inlineLoader = `data:text/javascript,${encodeURIComponent(newContent)}`;
90
- resolveData.request = inlineLoader;
91
- });
92
- });
93
- if (this.options.development)
94
- return;
95
- const replacements = [
96
- { target: ZERO_COM_CLIENT_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${this.compilationId}` },
97
- { target: ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${this.compilationId}` }
98
- ];
99
- compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
100
- compilation.hooks.processAssets.tap({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE }, (assets) => {
101
- for (const assetName in assets) {
102
- if (assetName.endsWith('.js')) {
103
- let assetSource = String(assets[assetName].source());
104
- let modified = false;
105
- replacements.forEach(({ target, replacement }) => {
106
- if (assetSource.includes(target)) {
107
- assetSource = assetSource.replaceAll(target, replacement);
108
- modified = true;
109
- }
110
- });
111
- if (modified) {
112
- compilation.updateAsset(assetName, new RawSource(assetSource));
113
- }
114
- }
115
- }
116
- });
117
- });
118
- }
119
- }
120
- exports.ZeroComWebpackPlugin = ZeroComWebpackPlugin;
121
- const formatMethodName = (funcName, path, line) => {
122
- return `${funcName}@${path}:${line}`;
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
123
15
  };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./lib/index"), exports);
@@ -0,0 +1,10 @@
1
+ export type Options = {
2
+ development?: boolean;
3
+ patterns: {
4
+ client: string;
5
+ server: string;
6
+ };
7
+ };
8
+ export declare const ZERO_COM_CLIENT_SEND = "ZERO_COM_CLIENT_SEND";
9
+ export declare const ZERO_COM_SERVER_REGISTRY = "ZERO_COM_SERVER_REGISTRY";
10
+ export declare const formatFuncIdName: (funcName: string, path: string, line: number) => string;
package/lib/common.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ 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';
5
+ exports.ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY';
6
+ const formatFuncIdName = (funcName, path, line) => {
7
+ return `${funcName}@${path}:${line}`;
8
+ };
9
+ exports.formatFuncIdName = formatFuncIdName;
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
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 ADDED
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,3 @@
1
+ import { Plugin } from 'rollup';
2
+ import { Options } from './common';
3
+ export declare function zeroComRollupPlugin(options: Options): Plugin;
package/lib/rollup.js ADDED
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.zeroComRollupPlugin = zeroComRollupPlugin;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const minimatch_1 = require("minimatch");
18
+ const path_1 = __importDefault(require("path"));
19
+ const ts_morph_1 = require("ts-morph");
20
+ const typescript_1 = __importDefault(require("typescript"));
21
+ 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
+ ];
31
+ return {
32
+ name: 'zero-com-rollup-plugin',
33
+ resolveId(source, importer, options) {
34
+ 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)
39
+ 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)
43
+ 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 {
59
+ return null;
60
+ }
61
+ return {
62
+ id: resolvedPath,
63
+ meta: {
64
+ isClient: requestedFromClient,
65
+ }
66
+ };
67
+ });
68
+ },
69
+ 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)
73
+ 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 && initializer instanceof ts_morph_1.ArrowFunction) {
96
+ if (decl.isExported() && initializer.isAsync()) {
97
+ const funcName = decl.getName();
98
+ const lineNumber = decl.getStartLineNumber();
99
+ const funcParams = initializer.getParameters().map(p => p.getName()).join(', ');
100
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
101
+ const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
102
+ initializer.setBodyText(newFunctionBody);
103
+ }
104
+ }
105
+ });
106
+ }
107
+ else {
108
+ const chunks = [];
109
+ sourceFile.getFunctions().forEach(func => {
110
+ if (func.isExported() && func.isAsync()) {
111
+ const funcName = String(func.getName());
112
+ const lineNumber = func.getStartLineNumber();
113
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
114
+ chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
115
+ }
116
+ });
117
+ sourceFile.getVariableDeclarations().forEach(decl => {
118
+ const initializer = decl.getInitializer();
119
+ if (initializer && initializer instanceof ts_morph_1.ArrowFunction) {
120
+ if (decl.isExported() && initializer.isAsync()) {
121
+ const funcName = decl.getName();
122
+ const lineNumber = decl.getStartLineNumber();
123
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(process.cwd(), id), lineNumber);
124
+ chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
125
+ }
126
+ }
127
+ });
128
+ if (chunks.length > 0) {
129
+ const textToAdd = `\nif (!global.${common_1.ZERO_COM_SERVER_REGISTRY}) global.${common_1.ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`;
130
+ sourceFile.insertText(sourceFile.getEnd(), textToAdd);
131
+ }
132
+ }
133
+ const result = project.emitToMemory();
134
+ const newContent = result.getFiles()[0].text;
135
+ return newContent;
136
+ },
137
+ renderChunk(code, chunk, options) {
138
+ if (development)
139
+ return null;
140
+ let modified = false;
141
+ let newCode = code;
142
+ replacements.forEach(({ target, replacement }) => {
143
+ if (newCode.includes(target)) {
144
+ newCode = newCode.replaceAll(target, replacement);
145
+ modified = true;
146
+ }
147
+ });
148
+ if (modified) {
149
+ return { code: newCode, map: null };
150
+ }
151
+ return null;
152
+ }
153
+ };
154
+ }
@@ -0,0 +1,15 @@
1
+ import { Compiler } from 'webpack';
2
+ import { Options } from './common';
3
+ export type Message = {
4
+ funcId: string;
5
+ params: any[];
6
+ [key: string]: any;
7
+ };
8
+ export declare class ZeroComWebpackPlugin {
9
+ private options;
10
+ private compilationId;
11
+ private clientPattern;
12
+ private serverPattern;
13
+ constructor(options: Options);
14
+ apply(compiler: Compiler): void;
15
+ }
package/lib/webpack.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ZeroComWebpackPlugin = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const minimatch_1 = require("minimatch");
9
+ const path_1 = __importDefault(require("path"));
10
+ const ts_morph_1 = require("ts-morph");
11
+ const typescript_1 = __importDefault(require("typescript"));
12
+ const common_1 = require("./common");
13
+ 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;
19
+ }
20
+ apply(compiler) {
21
+ const pluginName = ZeroComWebpackPlugin.name;
22
+ const { webpack } = compiler;
23
+ const { RawSource } = webpack.sources;
24
+ compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
25
+ 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)
29
+ 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 && initializer instanceof ts_morph_1.ArrowFunction) {
73
+ if (decl.isExported() && initializer.isAsync()) {
74
+ const funcName = decl.getName();
75
+ const lineNumber = decl.getStartLineNumber();
76
+ const funcParams = initializer.getParameters().map(p => p.getName()).join(', ');
77
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
78
+ const newFunctionBody = `return window.${common_1.ZERO_COM_CLIENT_SEND}({funcId: '${funcId}', params: [${funcParams}]})`;
79
+ initializer.setBodyText(newFunctionBody);
80
+ generatedFunctions.push(decl.getVariableStatementOrThrow().getText());
81
+ console.log('client:', funcId);
82
+ }
83
+ }
84
+ });
85
+ newModuleContent = generatedFunctions.join('\n\n');
86
+ }
87
+ else {
88
+ const chunks = [];
89
+ sourceFile.getFunctions().forEach(func => {
90
+ if (func.isExported() && func.isAsync()) {
91
+ const funcName = String(func.getName());
92
+ const lineNumber = func.getStartLineNumber();
93
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
94
+ chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
95
+ console.log('server:', funcId);
96
+ }
97
+ });
98
+ sourceFile.getVariableDeclarations().forEach(decl => {
99
+ const initializer = decl.getInitializer();
100
+ if (initializer && initializer instanceof ts_morph_1.ArrowFunction) {
101
+ if (decl.isExported() && initializer.isAsync()) {
102
+ const funcName = decl.getName();
103
+ const lineNumber = decl.getStartLineNumber();
104
+ const funcId = (0, common_1.formatFuncIdName)(funcName, path_1.default.relative(compiler.context, absolutePath), lineNumber);
105
+ chunks.push(`global.${common_1.ZERO_COM_SERVER_REGISTRY}['${funcId}'] = ${funcName}`);
106
+ console.log('server:', funcId);
107
+ }
108
+ }
109
+ });
110
+ newModuleContent = `${originalContent} if (!global.${common_1.ZERO_COM_SERVER_REGISTRY}) global.${common_1.ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`;
111
+ }
112
+ project.createSourceFile(absolutePath + '.ts', newModuleContent, { overwrite: true });
113
+ const result = project.emitToMemory();
114
+ const newContent = result.getFiles()[0].text;
115
+ const inlineLoader = `data:text/javascript,${encodeURIComponent(newContent)}`;
116
+ resolveData.request = inlineLoader;
117
+ });
118
+ });
119
+ if (this.options.development)
120
+ return;
121
+ const replacements = [
122
+ { target: common_1.ZERO_COM_CLIENT_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${this.compilationId}` },
123
+ { target: common_1.ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${this.compilationId}` }
124
+ ];
125
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
126
+ compilation.hooks.processAssets.tap({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE }, (assets) => {
127
+ for (const assetName in assets) {
128
+ if (assetName.endsWith('.js')) {
129
+ let assetSource = String(assets[assetName].source());
130
+ let modified = false;
131
+ replacements.forEach(({ target, replacement }) => {
132
+ if (assetSource.includes(target)) {
133
+ assetSource = assetSource.replaceAll(target, replacement);
134
+ modified = true;
135
+ }
136
+ });
137
+ if (modified) {
138
+ compilation.updateAsset(assetName, new RawSource(assetSource));
139
+ }
140
+ }
141
+ }
142
+ });
143
+ });
144
+ }
145
+ }
146
+ exports.ZeroComWebpackPlugin = ZeroComWebpackPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-com",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "main": "index.js",
5
5
  "repository": "https://github.com/yosbelms/zero-com",
6
6
  "keywords": [
@@ -12,7 +12,8 @@
12
12
  "client"
13
13
  ],
14
14
  "scripts": {
15
- "build": "npx tsc"
15
+ "build": "npx tsc -d",
16
+ "prepublishOnly": "npm run build"
16
17
  },
17
18
  "author": "",
18
19
  "license": "MIT",
@@ -23,9 +24,10 @@
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/webpack": "^5.28.5",
27
+ "rollup": "^4.52.5",
26
28
  "typescript": "^5.8.3"
27
29
  },
28
30
  "peerDependencies": {
29
31
  "typescript": "^5.0.0"
30
32
  }
31
- }
33
+ }
package/index.ts DELETED
@@ -1,154 +0,0 @@
1
- import fs from 'fs'
2
- import { minimatch } from 'minimatch'
3
- import path from 'path'
4
- import { Project } from 'ts-morph'
5
- import ts from 'typescript'
6
- import { Compiler } from 'webpack'
7
-
8
- type Options = {
9
- development?: boolean,
10
- patterns: {
11
- client: string,
12
- server: string,
13
- }
14
- }
15
-
16
- export type Message = {
17
- method: string,
18
- params: any[],
19
- [key: string]: any
20
- }
21
-
22
- export class ZeroComWebpackPlugin {
23
- private options: Options
24
- private compilationId: string
25
- private clientPattern: string
26
- private serverPattern: string
27
-
28
- constructor(options: Options) {
29
- this.options = {
30
- development: true,
31
- ...options,
32
- patterns: {
33
- ...options.patterns
34
- }
35
- }
36
- this.compilationId = String(Math.floor(Math.random() * 1000000))
37
- this.clientPattern = this.options.patterns.client
38
- this.serverPattern = this.options.patterns.server
39
- }
40
-
41
- apply(compiler: Compiler) {
42
- const ZERO_COM_CLIENT_SEND = 'ZERO_COM_CLIENT_SEND'
43
- const ZERO_COM_SERVER_REGISTRY = 'ZERO_COM_SERVER_REGISTRY'
44
-
45
- const pluginName = ZeroComWebpackPlugin.name
46
- const { webpack } = compiler
47
- const { RawSource } = webpack.sources
48
-
49
- compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
50
- nmf.hooks.beforeResolve.tap(pluginName, (resolveData) => {
51
- const absolutePath = path.resolve(resolveData.context, resolveData.request)
52
- const isServerFile = minimatch(absolutePath, path.join(compiler.context, this.serverPattern))
53
- if (!isServerFile) return
54
-
55
- const requestedFromClient = minimatch(resolveData.contextInfo.issuer, path.join(compiler.context, this.clientPattern))
56
-
57
- const tsPath = absolutePath + '.ts'
58
- const jsPath = absolutePath + '.js'
59
- const mjsPath = absolutePath + '.mjs'
60
- let resolvedPath = ''
61
-
62
- if (fs.existsSync(tsPath)) {
63
- resolvedPath = tsPath
64
- } else if (fs.existsSync(jsPath)) {
65
- resolvedPath = jsPath
66
- } else if (fs.existsSync(mjsPath)) {
67
- resolvedPath = mjsPath
68
- } else {
69
- throw new Error('Unable to resolve: ' + absolutePath)
70
- }
71
-
72
- const originalContent = fs.readFileSync(resolvedPath, 'utf8')
73
-
74
- const project = new Project({
75
- compilerOptions: {
76
- target: ts.ScriptTarget.ES2017,
77
- module: ts.ModuleKind.ESNext,
78
- },
79
- })
80
-
81
- const sourceFile = project.createSourceFile(absolutePath, originalContent, { overwrite: true })
82
- let newModuleContent = ''
83
-
84
- if (requestedFromClient) {
85
- const generatedFunctions: string[] = []
86
- sourceFile.getFunctions().forEach(func => {
87
- if (func.isExported() && func.isAsync()) {
88
- const funcName = String(func.getName())
89
- const lineNumber = func.getStartLineNumber()
90
- const funcParams = func.getParameters().map(p => p.getName()).join(', ')
91
- const method = formatMethodName(funcName, path.relative(compiler.context, absolutePath), lineNumber)
92
- const newFunctionBody = `return window.${ZERO_COM_CLIENT_SEND}({method: '${method}', params: [${funcParams}]})`
93
- func.setBodyText(newFunctionBody)
94
- generatedFunctions.push(func.getText())
95
- console.log('client:', method)
96
- }
97
- })
98
- newModuleContent = generatedFunctions.join('\n\n')
99
- } else {
100
- const chunks: string[] = []
101
- sourceFile.getFunctions().forEach(func => {
102
- if (func.isExported() && func.isAsync()) {
103
- const funcName = String(func.getName())
104
- const lineNumber = func.getStartLineNumber()
105
- const method = formatMethodName(funcName, path.relative(compiler.context, absolutePath), lineNumber)
106
- chunks.push(`global.${ZERO_COM_SERVER_REGISTRY}['${method}'] = ${funcName}`)
107
- console.log('server:', method)
108
- }
109
- })
110
- newModuleContent = `${originalContent} if (!global.${ZERO_COM_SERVER_REGISTRY}) global.${ZERO_COM_SERVER_REGISTRY} = Object.create(null); ${chunks.join(',')}`
111
- }
112
-
113
- project.createSourceFile(absolutePath + '.ts', newModuleContent, { overwrite: true })
114
- const result = project.emitToMemory()
115
- const newContent = result.getFiles()[0].text
116
- const inlineLoader = `data:text/javascript,${encodeURIComponent(newContent)}`
117
- resolveData.request = inlineLoader
118
- })
119
- })
120
-
121
- if (this.options.development) return
122
-
123
- const replacements = [
124
- { target: ZERO_COM_CLIENT_SEND, replacement: `__ZERO_COM_CLIENT_SEND_${this.compilationId}` },
125
- { target: ZERO_COM_SERVER_REGISTRY, replacement: `__ZERO_COM_SERVER_REGISTRY_${this.compilationId}` }
126
- ]
127
-
128
- compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
129
- compilation.hooks.processAssets.tap({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE }, (assets) => {
130
- for (const assetName in assets) {
131
- if (assetName.endsWith('.js')) {
132
- let assetSource = String(assets[assetName].source())
133
- let modified = false
134
- replacements.forEach(({ target, replacement }) => {
135
- if (assetSource.includes(target)) {
136
- assetSource = assetSource.replaceAll(target, replacement)
137
- modified = true
138
- }
139
- })
140
- if (modified) {
141
- compilation.updateAsset(assetName, new RawSource(assetSource))
142
- }
143
- }
144
- }
145
-
146
- })
147
- })
148
-
149
- }
150
- }
151
-
152
- const formatMethodName = (funcName: string, path: string, line: number) => {
153
- return `${funcName}@${path}:${line}`
154
- }