Spoofing Node dependencies?!

Let’s get Node-dependant packages to work without it!

When building any form, or processing any kind of user input, one should provide client-side validation before submitting data, of which would then (hopefully) be subject to server-side validation.

So for my next JavaScript-based, React Native project, I browsed around looking for a module that offered a neat schema description language and validator to handle my form validation, and ended up with Joi.

Even if you’re not planning on using Joi, I hope you’ll enjoy accompanying me on this midnight, problem solving adventure of trial and error, and discovery. ⭐️

Joi is a package that offers a nice way to add all sorts of definitions for the fields and values of my forms in a very clean, “JavaScript-esque” way.

const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
access_token: [Joi.string(), Joi.number()],
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email({ minDomainAtoms: 2 })
});

I prefer this concise, chained style to other styles offered by modules like revalidator , simply because I felt Joi provided a terse yet expressive way of defining my schemas.

const schema = {
properties: {
url: {
description: 'the url the object should be stored at',
type: 'string',
pattern: '^/[^#%&*{}\\:<>?\/+]+$',
required: true
},
challenge: {
description: 'a means of protecting data (insufficient for production, used as example)',
type: 'string',
minLength: 5
},
body: {
description: 'what to store at the url',
type: 'any',
default: null
}
}
});

Originally I was playing around with Joi’s schema structure and validation in a little node-based express app for request data validation and thought, “hey, this is pretty good, I want to use this for the frontend too!”

Supposed to be easy, right?

Just a matter of yarn add joi and import Joi from 'joi'; yeah?

Wrong.

Error: Unable to resolve module `crypto` from `/<project>/node_modules/hoek/lib/index.js`: Module `crypto` does not exist in the Haste module map

The crypto module? Inside of a hoek module?

The Crypto module isn’t something available on npm , so simply trying to yarn add crypto isn’t going to help here. That’s because it’s actually a standard library for node , where require('crypto') gives you access to various “cryptographic functionality that includes a set of wrappers for OpenSSL’s hash, HMAC, cipher, decipher, sign, and verify functions”.

Damn.

So Joi is a module that eventually requires node-exclusive dependencies to work, meaning it won’t work in a browser or for React Native… even though I’m not performing anything remotely close to cryptography.

I just want to validate my objects with a neatly defined schema!

Joi doesn't directly support browsers, but you could use joi-browser for an ES5 build of Joi that works in browsers

And I know that Joi acknowledges it’s “lack of support” for non-node environments, but surely that’s ridiculous! It doesn’t seem to me that Joi needs to do anything out of the realm of checking the values of inputs?

They do link to a unofficial bundling of Joi for the browser, but I wanted to focus on the officially maintained, up-to-date stream for Joi, and a mere browser incompatibility wasn’t going to get in the way of that for me!


Anyway let’s push on to the exciting bits and investigate the hoek module that introduces this need for the crypto module.

Unlike the crypto module, you can actually find Hoek on Github (and install it from npm). It is maintained by the Hapi.JS team—the same people behind Joi—and is described as a package containing “utility methods for the hapi ecosystem.”

Of course I investigated further and found where and how the crypto module was used in hoek to which it was lib/index.js and seems to be used only once after it’s imported.

Crypto.randomBytes(8).toString('hex')

Was I really locked in and defeated by joi simply because it’s dependency hoek needed the node-exclusive crypto module for simply creating a hex string for use in producing exports.uniqueFilename ?

I don’t want to do any of that; I just want to validate my objects…

What does joi even use hoek for?

Out of curiosity, I searched the joi repo on GitHub to find out:

const Hoek = require('hoek');
Hoek.assert(...
Hoek.assert(...
Hoek.clone(...
Hoek.assert(...
Hoek.assert(...
Hoek.assert(...
Hoek.flatten(...
Hoek.reach(...
Hoek.applyToDefaults(...

That’s it?

Nothing here seems to be even remotely node-exclusive!

If anything, there’s a good chance lodash can replace a lot of these…

If you're looking for a general purpose utility module, check out lodash or underscore.

Oh wait, that was fresh out of the hoek/README.md !

Surely I’m missing something—let’s investigate this handful of required exports, assert , clone , flatten , reach , applyToDefaults .

Seems like we could substitute:

And for assert, I’m sure there’s a more cross-environment, browser-friendly way to roll assertions.


Phew! After all that, here’s the situation:

  • I’ve got a package (joi ),
  • that requires another package (hoek ),
  • that requires another package (crypto ),
  • that can only be used in a node environment,
  • just so that my first package (joi ),
  • can use simple functions that are exposed by the second package (hoek),
  • of which could be easily substituted by more cross-environment friendly packages (lodash , assert).

And you know what? I’m still really excited to write my schemas in Joi, so let’s press onward to see if we can make this all click together!


Now, through all this investigating we see that the crypto object is only accessed when we use exports.uniqueFilename. So if we somehow replaced crypto with a “blank” of sorts, we could resolve this import error.

Given that hoek doesn’t use it for anything else (which it doesn’t) we should be in the clear if we wanted to substitute it.

And how might we perform this substitution?

If you are developing an application that uses a combination of Babel and Webpack, you could alias "crypto” to some sort of empty stub file (src/stub/index.js) in your project through your webpack.config.js file.

Unfortunately for me using React Native, merging (or even accessing) that kind of information into the bundling mechanism’s config isn’t possible, or in the case of create-react-app users, not possible without ejecting.

However, we are able to configure Babel with a .babelrc dot file.

Using a Babel plugin called module-resolver I was able to redirect the bundling mechanism to use a stub file (src/stub/index.js ) and pacify the complaints of unresolvable crypto module used by hoek .

After installing the plugin with yarn add -D babel-plugin-module-resolver I needed to add the plugin to the config and configure aliases to point crypto to the stub file.

{  
"plugins": [
["module-resolver", {
"root": ["."],
"alias": {
"crypto": "./src/stub"
}
}]
]
}

After a yarn start — reset-cache to restart the bundler with a clear cache (which is super important) — the redirection worked!

But, I ended up with yet another import error, like with crypto, for similarly node-exclusive modules: path and net .

After adding aliases for those modules in the same way as I did for crypto the bundler successfully spat out a working bundle!


Exception: Constructor Map requires 'new'

Now it’s a runtime error; turns out Joi wasn’t finished with me just yet.

So let’s explore the stack trace to find the error in joi/lib/types/symbol/index.js:16 :

internals.Map = class extends Map {

As we know that Map exists, considering the error isn’t about Map being undefined or similar, we now Google the error!

  • We find this,
  • which turns out to be a duplicate of this,
  • and amongst all the comments someone shared a link to this,
  • which is used to “enable extending builtin types like ‘Error’ and ‘Array’ and such, which require special treatment and require static analysis to detect.”

So thanks to another Babel plugin called babel-plugin-transform-builtin-extend we can now add that “special treatment” to fix the “way too poor implementation” by adding the plugin to .babelrc .

...
["transform-builtin-extend", {
"globals": ["Map"]
}]
...

And there we go, restart with a fresh cache and profit.

We did it! Finally…

I hope this helped some of you, and entertained others! Thanks for reading. 😁

For those interested here’s the final structure for the .babelrc:

...
"plugins": [
["module-resolver", {
"root": ["."],
"alias": {
"path": "./src/stub",
"crypto": "./src/stub",
"net": "./src/stub"
}
}],
["transform-builtin-extend", {
"globals": ["Map"]
}]
]
...