Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
nginx
GitHub Repository: nginx/nginx.org
Path: blob/main/xml/en/docs/njs/node_modules.xml
1 views
1
<?xml version="1.0"?>
2
3
<!--
4
Copyright (C) Nginx, Inc.
5
-->
6
7
<!DOCTYPE article SYSTEM "../../../../dtd/article.dtd">
8
9
<article name="Using node modules with njs"
10
link="/en/docs/njs/node_modules.html"
11
lang="en"
12
rev="6">
13
14
<section id="intro">
15
16
<para>
17
Often, a developer wants to use 3rd-party code,
18
usually available as a library of some kind.
19
In the JavaScript world, the concept of a module is relatively new,
20
so there was no standard until recently.
21
Many platforms (browsers) still don't support modules, which makes code
22
reuse harder.
23
This article describes ways to reuse
24
<link url="https://nodejs.org/">Node.js</link> code in njs.
25
</para>
26
27
<note>
28
Examples in this article use features that appeared in
29
<link doc="index.xml">njs</link>
30
<link doc="changes.xml" id="njs0.3.8">0.3.8</link>
31
</note>
32
33
<para>
34
There is a number of issues
35
that may arise when 3rd-party code is added to njs:
36
37
<list type="bullet">
38
39
<listitem>
40
Multiple files that reference each other and their dependencies
41
</listitem>
42
43
<listitem>
44
Platform-specific APIs
45
</listitem>
46
47
<listitem>
48
Modern standard language constructions
49
</listitem>
50
51
</list>
52
</para>
53
54
<para>
55
The good news is that such problems are not something new or specific to njs.
56
JavaScript developers face them daily
57
when trying to support multiple disparate platforms
58
with very different properties.
59
There are instruments designed to resolve the above-mentioned issues.
60
61
<list type="bullet">
62
63
<listitem>
64
Multiple files that reference each other, and their dependencies
65
<para>
66
This can be solved by merging all the interdependent code into a single file.
67
Tools like
68
<link url="http://browserify.org/">browserify</link> or
69
<link url="https://webpack.js.org/">webpack</link>
70
accept an entire project and produce a single file containing
71
your code and all the dependencies.
72
</para>
73
</listitem>
74
75
<listitem>
76
Platform-specific APIs
77
<para>
78
You can use multiple libraries that implement such APIs
79
in a platform-agnostic manner (at the expense of performance, though).
80
Particular features can also be implemented using the
81
<link url="https://polyfill.io/v3/">polyfill</link> approach.
82
</para>
83
</listitem>
84
85
<listitem>
86
Modern standard language constructions
87
<para>
88
Such code can be transpiled:
89
this means performing a number of transformations
90
that rewrite newer language features in accordance with an older standard.
91
For example, <link url="https://babeljs.io/"> babel</link> project
92
can be used to this purpose.
93
</para>
94
</listitem>
95
96
</list>
97
</para>
98
99
<para>
100
In this guide, we will use two relatively large npm-hosted libraries:
101
102
<list type="bullet">
103
104
<listitem>
105
<link url="https://www.npmjs.com/package/protobufjs">protobufjs</link>&mdash;
106
a library for creating and parsing protobuf messages used by the
107
<link url="https://grpc.io/">gRPC</link> protocol
108
</listitem>
109
110
<listitem>
111
<link url="https://www.npmjs.com/package/dns-packet">dns-packet</link>&mdash;
112
a library for processing DNS protocol packets
113
</listitem>
114
115
</list>
116
</para>
117
118
</section>
119
120
121
<section id="environment" name="Environment">
122
123
<para>
124
<note>
125
This document mostly employs a generic approach
126
and avoids specific best practice advices concerning Node.js
127
and JavaScript.
128
Make sure to consult the corresponding package's manual
129
before following the steps suggested here.
130
</note>
131
First (assuming Node.js is installed and operational), let's create an
132
empty project and install some dependencies;
133
the commands below assume we are in the working directory:
134
<example>
135
$ mkdir my_project &amp;&amp; cd my_project
136
$ npx license choose_your_license_here > LICENSE
137
$ npx gitignore node
138
139
$ cat &gt; package.json &lt;&lt;EOF
140
{
141
"name": "foobar",
142
"version": "0.0.1",
143
"description": "",
144
"main": "index.js",
145
"keywords": [],
146
"author": "somename &lt;[email protected]&gt; (https://example.com)",
147
"license": "some_license_here",
148
"private": true,
149
"scripts": {
150
"test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
151
}
152
}
153
EOF
154
$ npm init -y
155
$ npm install browserify
156
</example>
157
</para>
158
159
</section>
160
161
162
<section id="protobuf" name="Protobufjs">
163
164
<para>
165
The library provides a parser
166
for the <literal>.proto</literal> interface definitions
167
and a code generator for message parsing and generation.
168
</para>
169
170
<para>
171
In this example, we will use the
172
<link url="https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto">helloworld.proto</link>
173
file
174
from the gRPC examples.
175
Our goal is to create two messages:
176
<literal>HelloRequest</literal> and
177
<literal>HelloResponse</literal>.
178
We will use the
179
<link url="https://github.com/protobufjs/protobuf.js/blob/master/README.md#reflection-vs-static-code">static</link>
180
mode of protobufjs instead of dynamically generating classes, because
181
njs doesn't support adding new functions dynamically
182
due to security considerations.
183
</para>
184
185
<para>
186
Next, the library is installed and
187
the JavaScript code implementing message marshalling
188
is generated from the protocol definition:
189
<example>
190
$ npm install protobufjs
191
$ npx pbjs -t static-module helloworld.proto > static.js
192
</example>
193
</para>
194
195
<para>
196
Thus, the <literal>static.js</literal> file becomes our new dependency,
197
storing all the code we need to implement message processing.
198
The <literal>set_buffer()</literal> function contains code that uses the
199
library to create a buffer with the serialized
200
<literal>HelloRequest</literal> message.
201
The code resides in the <literal>code.js</literal> file:
202
<example>
203
var pb = require('./static.js');
204
205
// Example usage of protobuf library: prepare a buffer to send
206
function set_buffer(pb)
207
{
208
// set fields of gRPC payload
209
var payload = { name: "TestString" };
210
211
// create an object
212
var message = pb.helloworld.HelloRequest.create(payload);
213
214
// serialize object to buffer
215
var buffer = pb.helloworld.HelloRequest.encode(message).finish();
216
217
var n = buffer.length;
218
219
var frame = new Uint8Array(5 + buffer.length);
220
221
frame[0] = 0; // 'compressed' flag
222
frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24; // length: uint32 in network byte order
223
frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
224
frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt; 8;
225
frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt; 0;
226
227
frame.set(buffer, 5);
228
229
return frame;
230
}
231
232
var frame = set_buffer(pb);
233
</example>
234
</para>
235
236
<para>
237
To ensure it works, we execute the code using node:
238
<example>
239
$ node ./code.js
240
Uint8Array [
241
0, 0, 0, 0, 12, 10,
242
10, 84, 101, 115, 116, 83,
243
116, 114, 105, 110, 103
244
]
245
</example>
246
You can see that this got us a properly encoded <literal>gRPC</literal> frame.
247
Now let's run it with njs:
248
<example>
249
$ njs ./code.js
250
Thrown:
251
Error: Cannot find module "./static.js"
252
at require (native)
253
at main (native)
254
</example>
255
</para>
256
257
<para>
258
Modules are not supported, so we've received an exception.
259
To overcome this issue, let's use <literal>browserify</literal>
260
or other similar tool.
261
</para>
262
263
<para>
264
An attempt to process our existing <literal>code.js</literal> file will result
265
in a bunch of JS code that is supposed to run in a browser,
266
i.e. immediately upon loading.
267
This isn't something we actually want.
268
Instead, we want to have an exported function that
269
can be referenced from the nginx configuration.
270
This requires some wrapper code.
271
<note>
272
In this guide, we use
273
njs <link doc="cli.xml">cli</link> in all examples for the sake of simplicity.
274
In real life, you will be using nginx njs module to run your code.
275
</note>
276
</para>
277
278
<para>
279
The <literal>load.js</literal> file contains the library-loading code that
280
stores its handle in the global namespace:
281
<example>
282
global.hello = require('./static.js');
283
</example>
284
This code will be replaced with merged content.
285
Our code will be using the "<literal>global.hello</literal>" handle to access
286
the library.
287
</para>
288
289
<para>
290
Next, we process it with <literal>browserify</literal>
291
to get all dependencies into a single file:
292
<example>
293
$ npx browserify load.js -o bundle.js -d
294
</example>
295
The result is a huge file that contains all our dependencies:
296
<example>
297
(function(){function......
298
...
299
...
300
},{"protobufjs/minimal":9}]},{},[1])
301
//# sourceMappingURL..............
302
</example>
303
To get final "<literal>njs_bundle.js</literal>" file we concatenate
304
"<literal>bundle.js</literal>" and the following code:
305
<example>
306
// Example usage of protobuf library: prepare a buffer to send
307
function set_buffer(pb)
308
{
309
// set fields of gRPC payload
310
var payload = { name: "TestString" };
311
312
// create an object
313
var message = pb.helloworld.HelloRequest.create(payload);
314
315
// serialize object to buffer
316
var buffer = pb.helloworld.HelloRequest.encode(message).finish();
317
318
var n = buffer.length;
319
320
var frame = new Uint8Array(5 + buffer.length);
321
322
frame[0] = 0; // 'compressed' flag
323
frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24; // length: uint32 in network byte order
324
frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
325
frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt; 8;
326
frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt; 0;
327
328
frame.set(buffer, 5);
329
330
return frame;
331
}
332
333
// functions to be called from outside
334
function setbuf()
335
{
336
return set_buffer(global.hello);
337
}
338
339
// call the code
340
var frame = setbuf();
341
console.log(frame);
342
</example>
343
Let's run the file using node to make sure things still work:
344
<example>
345
$ node ./njs_bundle.js
346
Uint8Array [
347
0, 0, 0, 0, 12, 10,
348
10, 84, 101, 115, 116, 83,
349
116, 114, 105, 110, 103
350
]
351
</example>
352
Now let's proceed further with njs:
353
<example>
354
$ njs ./njs_bundle.js
355
Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103]
356
</example>
357
The last thing will be to use njs-specific API to convert
358
array into byte string, so it could be usable by nginx module.
359
We can add the following snippet before the line
360
<literal>return frame; }</literal>:
361
<example>
362
if (global.njs) {
363
return String.bytesFrom(frame)
364
}
365
</example>
366
Finally, we got it working:
367
<example>
368
$ njs ./njs_bundle.js |hexdump -C
369
00000000 00 00 00 00 0c 0a 0a 54 65 73 74 53 74 72 69 6e |.......TestStrin|
370
00000010 67 0a |g.|
371
00000012
372
</example>
373
This is the intended result.
374
Response parsing can be implemented similarly:
375
<example>
376
function parse_msg(pb, msg)
377
{
378
// convert byte string into integer array
379
var bytes = msg.split('').map(v=>v.charCodeAt(0));
380
381
if (bytes.length &lt; 5) {
382
throw 'message too short';
383
}
384
385
// first 5 bytes is gRPC frame (compression + length)
386
var head = bytes.splice(0, 5);
387
388
// ensure we have proper message length
389
var len = (head[1] &lt;&lt; 24)
390
+ (head[2] &lt;&lt; 16)
391
+ (head[3] &lt;&lt; 8)
392
+ head[4];
393
394
if (len != bytes.length) {
395
throw 'header length mismatch';
396
}
397
398
// invoke protobufjs to decode message
399
var response = pb.helloworld.HelloReply.decode(bytes);
400
401
console.log('Reply is:' + response.message);
402
}
403
</example>
404
</para>
405
406
</section>
407
408
409
<section id="dnspacket" name="DNS-packet">
410
411
<para>
412
This example uses a library for generation and parsing of DNS packets.
413
This a case worth considering because the library and its dependencies
414
use modern language constructions not yet supported by njs.
415
In turn, this requires from us an extra step: transpiling the source code.
416
</para>
417
418
<para>
419
Additional node packages are needed:
420
<example>
421
$ npm install @babel/core @babel/cli @babel/preset-env babel-loader
422
$ npm install webpack webpack-cli
423
$ npm install buffer
424
$ npm install dns-packet
425
</example>
426
The configuration file, webpack.config.js:
427
<example>
428
const path = require('path');
429
430
module.exports = {
431
entry: './load.js',
432
mode: 'production',
433
output: {
434
filename: 'wp_out.js',
435
path: path.resolve(__dirname, 'dist'),
436
},
437
optimization: {
438
minimize: false
439
},
440
node: {
441
global: true,
442
},
443
module : {
444
rules: [{
445
test: /\.m?js$$/,
446
exclude: /(bower_components)/,
447
use: {
448
loader: 'babel-loader',
449
options: {
450
presets: ['@babel/preset-env']
451
}
452
}
453
}]
454
}
455
};
456
</example>
457
Note we are using "<literal>production</literal>" mode.
458
In this mode webpack does not use "<literal>eval</literal>" construction
459
not supported by njs.
460
The referenced <literal>load.js</literal> file is our entry point:
461
<example>
462
global.dns = require('dns-packet')
463
global.Buffer = require('buffer/').Buffer
464
</example>
465
We start the same way, by producing a single file for the libraries:
466
<example>
467
$ npx browserify load.js -o bundle.js -d
468
</example>
469
Next, we process the file with webpack, which itself invokes babel:
470
<example>
471
$ npx webpack --config webpack.config.js
472
</example>
473
This command produces the <literal>dist/wp_out.js</literal> file, which is a
474
transpiled version of <literal>bundle.js</literal>.
475
We need to concatenate it with <literal>code.js</literal>
476
that stores our code:
477
<example>
478
function set_buffer(dnsPacket)
479
{
480
// create DNS packet bytes
481
var buf = dnsPacket.encode({
482
type: 'query',
483
id: 1,
484
flags: dnsPacket.RECURSION_DESIRED,
485
questions: [{
486
type: 'A',
487
name: 'google.com'
488
}]
489
})
490
491
return buf;
492
}
493
</example>
494
Note that in this example generated code is not wrapped into function and we
495
do not need to call it explicitly.
496
The result is in the "<literal>dist</literal>" directory:
497
<example>
498
$ cat dist/wp_out.js code.js > njs_dns_bundle.js
499
</example>
500
Let's call our code at the end of a file:
501
<example>
502
var b = set_buffer(global.dns);
503
console.log(b);
504
</example>
505
And execute it using node:
506
<example>
507
$ node ./njs_dns_bundle_final.js
508
Buffer [Uint8Array] [
509
0, 1, 1, 0, 0, 1, 0, 0,
510
0, 0, 0, 0, 6, 103, 111, 111,
511
103, 108, 101, 3, 99, 111, 109, 0,
512
0, 1, 0, 1
513
]
514
</example>
515
Make sure this works as expected, and then run it with njs:
516
<example>
517
$ njs ./njs_dns_bundle_final.js
518
Uint8Array [0,1,1,0,0,1,0,0,0,0,0,0,6,103,111,111,103,108,101,3,99,111,109,0,0,1,0,1]
519
</example>
520
521
</para>
522
523
<para>
524
The response can be parsed as follows:
525
<example>
526
function parse_response(buf)
527
{
528
var bytes = buf.split('').map(v=>v.charCodeAt(0));
529
530
var b = global.Buffer.from(bytes);
531
532
var packet = dnsPacket.decode(b);
533
534
var resolved_name = packet.answers[0].name;
535
536
// expected name is 'google.com', according to our request above
537
}
538
</example>
539
</para>
540
541
</section>
542
543
</article>
544
545