I'm working on a project that uses typescript on both backend (nodejs) and front-end. The project has some cryptography involved which means I'm using WebCrypto - on the backend I use node-webcrypto-ossl as a shim so I may share code between the client and the server.
Is there a way to make a module export one thing on the client and another on the server so I may just do import * as crypto from './webcrypto' to expose a the interface in a common manner?
On the server it should export node-webcrypto-ossl and on the client just expose window.crypto.
I tried various things but webpack keeps attempting to pull node-webcrypto-ossl into the browser which unsurprisingly fails.
Here's my (failed) attempt:
let crypto = null;
if (typeof window === 'undefined') {
const WebCrypto = require('node-webcrypto-ossl');
crypto = new WebCrypto();
} else {
crypto = window.crypto;
}
export {
crypto as webcrypto
};
Approach itself is mostly correct, you may need configure webpack's externals to keep node-webcrypto-ossl to be loaded in nodejs context only. Shortest pseudo looks like
webpack.config.js
...
externals: {
'node-webcrypto-ossl: {
commonjs: 'node-webcrypto-ossl'
},
then webpack will not try to bundle specific module but leave require as is for those modules.
In addition to those, you may able to configure definePlugin as well for node.js / browser context so your crypto module can be statically compiled for each environment in build time, instead of looking object in runtime.
Related
I have a meteor repo with server and client code imports which resolved in bundling time:
// src/models/messages
import { Meteor } from 'meteor/meteor';
if (Meteor.isClient) {
module.exports = require('./client/index.js');
}
if (Meteor.isServer) {
module.exports = require('./server/index.js');
}
As can be seen, I have a message model that imported in client code and server code, with the same path.
When I develop with VSCode, IntelliSense couldn`t resolve such imports and I can use all power of the IDE.
Is there any way to configure IntelliSense to map source code with current environment? For instance, if I work in server code, when import { Messages } from '../../models/messages' should be resolved as src/models/messages/server/index.js?
After some research I have concluded that it is not possible.
Best way is split server and client code with different imports. This should protect from unpredictable the code execution.
However, if the code on server and client shares same contracts (interfaces) when it will be better to declare abstract classes and implement them in client code and server code. Then it will be possible to make such conditional import and protect yourself from runtime errors.
There is the isomorphic-webcrypto that pretends doing that but doesn't : it builds separate build for each target.
There is the noble-crypto way to do it, but it's based on if-else conditions and fails if I want an isomorphic mjs code.
Finally, there is the eval require way way to pass-through bundler, but node fails to use it in mjs.
In brief :
const crypto = require("crypto"); // work only in node.js but not in mjs file.
const crypto = eval(`require("crypto")`); // pass-thru bundler, then work only in node.js but not in mjs file.
window.crypto; // work only in browser
import * as crypto from "crypto"; // could work from both but must be at top level of a module, so it can't be a conditional import.
I would like to use native crypto in node.js and in browser, in an isomorphic way, to be able to use native import mjs in node and browser transparently.
How can I do this?
Alright. Ready for something ugly? :-) Behold, my latest hackjob… IsomorphicCyrpto.js:
export default
globalThis.crypto ||
(await import('node:crypto')).default.webcrypto
;
This works in Node.js v16 in module mode ("type": "module" in package.json, or equivalent CLI args), and will probably work with your bundler too… but who knows. ;-) Anyone using this code snippet should test thoroughly on whatever platforms they want to use it on.
In a nutshell:
We use globalThis, which represents global under Node.js, window for most browser contexts, and could perhaps even be a Worker context.
We first check to see if crypto is a thing. If it is, we're probably on a browser, and can just use that directly.
If crypto is not a thing, we're probably on Node.js and we need to import the module.
Because this is done dynamically, we need a dynamic import() rather than a true import.
import() is async and returns a Promise. But hey, it's all good, because top-level await is a thing in Node.js now!
To then use the module:
import crypto from './lib/IsomorphicCrypto.js';
console.log( crypto.randomUUID() );
Uggggly, but works for now. Hopefully, someone comes up with a better solution, or Node.js and browser contexts converge on naming in the future.
Is there an existing API or library that can be used to load a JSON file in both the browser and Node?
I'm working on a module that I intend to run both from the command-line in NodeJS, and through the browser. I'm using the latest language features common to both (and don't need to support older browsers), including class keywords and the ES6 import syntax. The class in question needs to load a series of JSON files (where the first file identifies others that need to be loaded), and my preference is to access them as-is (they are externally defined and shared with other tools).
The "import" command looks like it might work for the first JSON file, except that I don't see a way of using that to load a series of files (determined by the first file) into variables.
One option is to pass in a helper function to the class for loading files, which the root script would populate as appropriate for NodeJS or the browser.
Alternatively, my current leading idea, but still not ideal in my mind, is to define a separate module with a "async function loadFile(fn)" function that can be imported, and set the paths such that a different version of that file loads for browser vs NodeJS.
This seems like something that should have a native option, or that somebody else would have already written a module for, but I've yet to find either.
For node, install the node-fetch module from npm.
Note that browser fetch can't talk directly to your filesystem -- it requires an HTTP server on the other side of the request. Node can talk to your filesystem, as well as making HTTP calls to servers.
It sounds like as of now, there is no perfect solution here. The 'fetch' API is the most promising, but only if Node implements it some day.
In the meantime I've settled for a simple solution that works seamlessly with minimal dependencies, requiring only a little magic with my ExpressJS server paths to point the served web instance to a different version of utils.js.
Note: To use the ES-style import syntax for includes in NodeJS (v14+) you must set "type":"module" in your package.json. See https://nodejs.org/api/esm.html#esm_package_json_type_field for details. This is necessary for true shared code bases.
Module Using it (NodeJS + Browser running the same file):
import * as utils from "../utils.js";
...
var data = await utils.loadJSON(filename);
...
utils.js for browser:
async function loadJSON(fn) {
return $.getJSON(fn); // Only because I'm using another JQuery-dependent lib
/* Or natively something like
let response = await fetch(fn);
return response.json();
*/
}
export { loadJSON };
utils.js for nodeJS
import * as fs from 'fs';
async function loadJSON(fn) {
return JSON.parse(await fs.promises.readFile(fn));
}
export { loadJSON };
I have a client-side application which makes use of some browser global properties like Element or document.
I'd like to run my application in node.js as well and currently I am overriding those globals with the domino dom implementation in my server like so:
const domino = require("domino");
const domimpl = domino.createDOMImplementation();
const document = domimpl.createHTMLDocument();
Object.assign(global, Element: domino.impl.Element, document};
const myApp = require('my-app');
I am currently using rollup to bundle different versions of my-app, how can I have rollup do this for me automatically for the _server version of my-app so consumers of my-app don't have to do that?
I was thinking of writing my own rollup plugin but I feel like overriding globals seems like a common practice.
TLDR; Use separate entry file instead of a rollup plugin.
Simply add the following instead of a rollup plugin
if (typeof window ==== "undefined") {
const domino = require("domino");
const domimpl = domino.createDOMImplementation();
const document = domimpl.createHTMLDocument();
Object.assign(global, Element: domino.impl.Element, document};
}
// my-app code
You might be worried about domino entering client side code. To fix this, use separate bundles for server and client, wrap the above mocking code in a separate file and use the following in your my-app’s main file meant for the server bundle, an approach similar to how React ships production and development bundles - conditional imports.
Server main file
require(‘./globals-mocking’);
// my-app client code
Client main file
// my-app client code only
package’s main file
if (SERVER_ONLY) {
module.exports = require('./my-app-server.js');
} else {
module.exports = require('./my-app-client.js');
}
Use rollup's replace plugin and define SERVER_ONLY (https://github.com/rollup/rollup-plugin-replace#usage) for server entry only. If you use UglifyJS or simlilar tool that eliminates dead code, you wont have domino and duplicated server code.
EDIT: Noticed a minor issue. Condition should be if (SERVER_ONLY) {. Use the following definition along with it for the server entry file.
plugins: [
replace({
SERVER_ONLY: JSON.stringify(true)
})
]
What is the best way to create an ES6 library, e.g. my-es6-crypto-lib, which can be used both in the browser and in Node.js, but for which the implementations are different on each platform?
(E.g. the Node.js implementation uses the built-in crypto module for better performance.)
ES6 module usage:
import { sha256 } from 'my-es6-crypto-lib'
let digest = sha256('abc')
console.log(digest)
Or Node.js-style requires:
let sha256 = require('my-es6-crypto-lib')
let digest = sha256('abc')
console.log(digest)
The package.json for my-es6-crypto-lib would include:
{
"name": "my-es6-crypto-lib",
"main": "transpiled-to-commonjs/node.js",
"module": "es6-module/node.js",
"browser": "es6-module/browser.js",
...
}
Node.js will follow the main key to resolve the CommonJS module.
Tools capable of consuming ES6 modules (like transpiler/bundling tools) follow the module key.
Tools which consume ES6 modules and bundle them for browsers (e.g. rollup-plugin-node-resolve) will follow the browser key.
The actual implementation for Node.js would look something like: (transpiled-to-commonjs/node.js)
// built-in module, faster than the pure Javascript implementation
let createHash = require('crypto')
export function sha256 (message) {
return createHash('sha256').update(message).digest('hex')
}
While the browser implementation would look something like: (es6-module/browser.js)
// a javascript-only implementation available at es6-module/hashFunctions.js
import { sha256 } from './hashFunctions'
export function sha256 (message) {
// slightly different API than the native module
return sha256(message, 'hex')
}
Note, the implementation of each function is different on each platform, but both sha256 methods have the same parameter message and return a string.
What is the best way to structure my ES6 module to provide each of these implementations? Are there any javascript libraries out there which do this?
Ideally:
the unused implementation should be able to be tree shaken, and
no runtime checks should be used to determine the current environment.
(I also opened a GitHub issue for Rollup →)
After a while, I now think the best way to do this is actually to export different functionality for each environment.
In the past, complex javascript libraries have used solutions like Browserify to bundle a version of their application for the browser. Most of these solutions work by allowing library developers to extensively configure and manually override various dependencies with respective browser versions.
For example, where a Node.js application might use Node.js' built-in crypto module, a browser version would need to fall back to a polyfill-like alternative dependency like crypto-browserify.
With es6, this customization and configuration is no longer necessary. Your library can now export different functionality for different consumers. While browser consumers may import a native JavaScript crypto implementation which your library exports, Node.js users can choose to import a different, faster implementation which your library exports.
...
It's explained in depth, and with an example, in the typescript-starter readme →