How To Compile Node.js Scripts With Google Closure Compiler

Art Deco
Art Deco
Dec 4, 2018 · 4 min read

The problem that anyone who was to build Node.js modules with the Closure library is that it will not let them do it because it will not know the internal modules that might be imported in code, e.g., child_process or path, and result in an error:

google-closure-compiler/cli.js — js t/error.js — module_resolution NODE
t/error.js:1: ERROR — Failed to load module “child_process”
import { fork } from ‘child_process’
^
1 error(s), 0 warning(s)

Therefore, we have to let the compiler know about the child_process package by placing it in the node_modules folder of the project directory and passing its package.json with the --js flag. The innovation of this approach is to place mocks of the default Node.js built-ins into node_modules directory, and provide required externs.

The content of the package.json file placed in the node_modules/child_process. With out script, we will not create a mock package if one is already present in the node_modules directory, or unless it was generated for another version of Node.js. The tool supports core v8 and v10 modules.

By faking a built-in module, we can allow the code to compile, and then wrap it to run in a scope with DEPACK$child_process. The disadvantage of this process is that the VS Code will try to read from node_modules and when the module is found there, the TypeScript behind VS Code can it up as a module and not a built-in, and not provide hints. Therefore, it might required to delete the mocked modules after the compilation so not to break the Developer Experience, however sometimes the VS Code does not react to this thing and it’s uncertain (please post in comments what you have found maybe w/ other editors as well).

So all we need to do now is to add a wrapper. However, one step that we haven’t discussed is creating an extern using the contrib folder from the Closure npm package.

Creating A Child_Process Extern

The aim of this post is to show that it is possible to build Node.js projects using Google Closure Library. We create a local extern file copied from the contrib of the Closure library, but rename the global that it defines to DEPACK$child_processes. It will have JSDoc annotation left-over, but it is not as important because it is only used for type checking i.e. warnings, but the purpose of including the extern is so that the compiler knows how to properly rename the properties of objects and to correctly use those properties. To use the closure types for type checking is not the purpose of this post and you can read about it later, but’s it’s just JSDoc on steroids. When describing externs, however, every property of the module needs to be specified with the dot notation form (e.g., DEAPCK$path.resolve) to not get scrambled by the compiler.

We have to update the supplied extern with the ForkOptions type otherwise not included in the contrib.

/**
* @typedef {{cwd: string, env: Object.<string,*>, execPath: string, execArgv: Array<string>, silent: boolean, stdio: (Array|string), windowsVerbatimArguments: boolean, uid: number, gid: number}}
*/
DEPACK$child_process.ForkOptions

Because the child_process extern depends on the stream and the events modules (so that we can call the .on listener on the spawned process), we also pass them as externs with the--externs flag in our program. The full source code to those externs can be found by the links below. The externs do not need to be added with --js and this technique is known as proxying built-in modules with preambled requires.

Creating The Wrapper

The lynchpin of this approach is the preamble code — similar to the one used in Browserify but more simple. During the ADVANCED compilation, all imports are reduced into a single file, and we are not compiling modules for exports in this story (only an executable script — read later).

We will use the wrapper around compiled code to require the child_process module into the DEPACK$child_processes variable. Otherwise, there is no way to run Node.js modules (ok let me know one ok?)… The wrapper can be written as an argument passed in the--output_wrapper, however, we do it programmatically with the Depack.

Compiling The Code

We now pass all necessary arguments to the compiler with the NODE as the module resolution mode.

java —-compilation_level=ADVANCED —-language_in=ECMASCRIPT_2018 —-language_out=ECMASCRIPT_2017 —-module_resolution=NODE —-formatting=PRETTY_PRINT —-warning_level=QUIET —-externs=externs/Buffer.js--externs=externs/child_process.js --externs=externs/stream.js --externs=externs/events.js
--js=node_modules/child_process/index.js
--js=node_modules/child_process/package.json
--process_common_js_modules=t/error.js

The code that we want to compile again:

We add the Buffer extern where we keep the process definitions (global Node.js externs which are otherwise unknown to the compiler even in the NODE mode).

The forked module will just print to stdout and stderr.

$ depack t/error.js -o tt.js

$ node tt

As you can see, the program runs correctly and prints both the stdout and stderr properties of the forked process.

The Possibility Of Compiling Node.js Built-Ins W/ Closure Compiler — running V8 JS Stripped Of Bloatware.

Theoretically, it would be possible to compile just the needed internals from Node.js source code, meaning that all bundled built-ins which are not used (e.g., crypto) will be left out and there will be some raw JavaScript to execute. However, Node.js has the even loop and it needs to be investigated more.

Art Deco

Written by

Art Deco

Node.js tools for the best developer experience. https://artd.eco https://nodetools.co

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade