Conditional Module Inclusion

Hi, I’m working on a project with builds for Browser, React Native, and Electron. I’ve written a seperate IO module for each platform, each module has the same interface so they can be used interchangeably. I’d like to be able to switch module is being used at bundle time, while retaining type safety

I tried using Old Docs, but this can only target one platform at a time, and I’d like to develop on multiple platforms simultaniously.

I’d like an output structure sort of like Inlining Constants | ReScript Language Manual, but that sort of conditional doesn’t seem to work at the module level.

I’m not averse to writing a thin shim layer in js, but I would then need a way to re-apply a module type to that file, which I’m not sure how to do.

Any ideas on how I can achieve what I’m looking for??

Does First Class Modules cover your desired use case?

You could include the target module like this:

let getPlatform = (target): module(PLATFORM) => {
  switch target {
  | #browser => module(Browser)
  | #native => module(Native)
  | #electron => module(Electron)
  }
}

module Main = {
  include unpack(getPlatform(#browser))
}

Complete example on Playground.

1 Like

Seems like a thing to be done with functors but I havnt seen it done that extensively.
Could you separate into different packages and include at the yaml/npm level?

I don’t see any reason why this wouldn’t work. E.g., say you have files arranged like:

  • src/io/IO__Target.ml
  • src/io/target/IO__Target__Browser.res
  • src/io/target/IO__Target__ReactNative.res
  • src/io/target/IO__Target__Electron.res

Then in src/io/IO__Target.ml you can define:

module Browser = IO__Target__Browser
module ReactNative = IO__Target__ReactNative
module Electron = IO__Target__Electron

module type S = sig ... end

module Make(IO : S) = struct ... end

module Platform = Make(
#if defined TARGET && TARGET = "browser" then
Browser
#else if defined TARGET && TARGET = "reactnative" then
ReactNative
#else if defined TARGET && TARGET = "electron" then
Electron
#end
)

You can also experiment with different file and folder layouts, but this should be a good starting point. To build for each platform just pass in the env variable for the platform, e.g. TARGET=browser rescript.

1 Like

Thanks for the help ya’ll I’ve played around a bit further and ended up with this solution:

@val external forceInlineRequire: string => module(IoBase.Interface) = "require"

let getPlatform = (target): module(IoBase.Interface) => {
  switch target {
  | #browser => forceInlineRequire("./IoBrowser.bs")
  | #native => forceInlineRequire("./IoNative.bs")
  | #electron => forceInlineRequire("./IoElectron.bs")
  }
}

module SingletonDataDriver = IoBase.MakeSingletonDataDriver(unpack(getPlatform(#browser)))
module InstancedDataDriver = IoBase.MakeInstancedDataDriver(unpack(getPlatform(#browser)))

which comiples to the following:

var IoBase = require("./IoBase.bs.js");

function getPlatform(target) {
  if (target === "native") {
    return require("./IoNative.bs");
  } else if (target === "browser") {
    return require("./IoBrowser.bs");
  } else {
    return require("./IoElectron.bs");
  }
}

var SingletonDataDriver = IoBase.MakeSingletonDataDriver(getPlatform("browser"));

var InstancedDataDriver = IoBase.MakeInstancedDataDriver(getPlatform("browser"));

exports.getPlatform = getPlatform;
exports.SingletonDataDriver = SingletonDataDriver;
exports.InstancedDataDriver = InstancedDataDriver;

Which I’m pretty sure should allow the bundler to select only the actual module I need at bundle-time!

2 Likes