Search code examples
node.jstypescriptnext.jsnext.js14

next.js, how to import different modules depending on environment


I have two modules 'web,ts' and 'node.ts' with identical interfaces. The first is meant to run on both the client side and the edge environment while the other relies on node:crypto.

I would like to have a single module (dyna.ts) that acts as a front for both that I can import from anywhere in the project without worrying about the current environment.

I have attempted this but the compiler always crashes, as an example my last attempt:

import webBox from "./web";

type CryptoBox = {
  hash: (data: string, salt: string, ...moreSalt: string[]) => Promise<string>;
  random: (range: bigint) => bigint;
};

let _hash: CryptoBox["hash"] | undefined;
let _random: CryptoBox["random"] | undefined;

if (typeof crypto === "undefined" && process.release.name === "node") {
  try {
    let box = require("./node");
    _hash = box.hash;
    _random = box.random;
  } catch (error) {
    if (typeof webBox.hash !== "undefined") _hash = webBox.hash;
    if (typeof webBox.random !== "undefined") _random = webBox.random;
  }
} else {
  if (typeof webBox.hash !== "undefined") _hash = webBox.hash;
  if (typeof webBox.random !== "undefined") _random = webBox.random;
}

if (typeof _hash === "undefined") throw new Error("no hash function available");

if (typeof _random === "undefined")
  throw new Error("no random function available");

export const hash = _hash;
export const random = _random;

note1: The type CryptoBox is the type of the default export of both './node.ts' and './web.ts'

note2: Checking typeof window === undefined is useless here as the edge runtime lacks a window but cannot use node:crypto, and even if it did work require("./node") crashes the compiler when wrapped in a if(false).

note3: (await import()).default crashes the same way require()

Hopefully this is possible so I don't have to maintain 3 identical modules (except for what they import) to do basic verification on data passed around by the client middleware and node server.

ps: the error message:

node:crypto
Module build failed: UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
Import trace for requested module:
node:crypto
./src/lib/crypto/node.ts
./src/lib/crypto/dyna.ts
./src/lib/server_only/auth/login.ts

pps: (for Mike) the older variant I tried first:

type CryptoBox={
    hash: (data: string, salt: string, ...moreSalt: string[]) => Promise<string>;
    random: (range: bigint) => bigint;
}
let box:CryptoBox;
try{
    box=require("./node");
}catch(error){
    box=require("./web");
}
export const hash=box.hash;
export const random=box.random;

Solution

  • The function webpack.NormalModuleReplacementPlugin along with the webpack function arguments found next.js's Custom Webpack Config documentation solve this problem using the following next.config.js file:

    const webpack = require('webpack');
    /**
     * @typedef {Object} NextJsConfigContext
     * @property {undefined|"nodejs"|"web"} nextRuntime - Specifies the runtime environment, e.g., "nodejs" or "web". (undefined for client side)
     */
    module.exports = {
        /**
         * @param {import('webpack').Configuration} config - The webpack configuration object.
         * @param {NextJsConfigContext} context - The context object for the Next.js configuration.
         * @returns {import('webpack').Configuration} - The modified webpack configuration object.
         */
        webpack: (config, {nextRuntime}) => {
            const interfaceEnv=(nextRuntime==="nodejs")?"-node":"-web";
            config.plugins.push(
                new webpack.NormalModuleReplacementPlugin(
                    /(.*)-WebOrNode(\.*)/,
                    function (resource){
                        resource.request = resource.request.replace(
                            "-WebOrNode",
                            interfaceEnv
                        )
                    }
                )
            );
            return config;
        }
    };
    

    Now by re-naming node.ts and web.ts to core-node.ts and core-web.ts respectively, dyna.ts is reduced to:

    import { CryptoBox } from "../types";
    //@ts-expect-error
    import _box from './core-WebOrNode';//! points to different files depending on the environment (core-WebOrNode.jd is non-extant)
    const box:CryptoBox=_box;
    export default box;
    

    (or just import from the non-existent './core-WebOrNode' file directly in the same way dyna.ts would instead of using it as a wrapper)