Standardised exotic modules RFC

Note, this is mostly an expansion on my comment here.

TL;DR: The current exotic module rules are fiddly, difficult to reason about, and not flexible enough for many use cases. I propose a standardised way to mark a ReScript module as exotic to solve these problems.

Problems with the current exotic module system

  • Exotic modules support some special characters but not all. It’s not clear what and why certain things are supported or not. Modules such as $foo.res don’t compile
  • They are currently not a complete solution to support file-system-based routing in frameworks such as Remix and Next.js. This is because
    • Duplicate module names are not allowed. A ReScript project with foo/[id].res and baz/[id].res doesn’t compile.
    • Following on from the very first point, some conventions like $id.res are not possible to use in ReScript right now

Proposed solution

Right now an “exotic module” is a set of loosely defined rules that exist only in the ReScript compiler source code. We should formalise the definition of exotic modules to simply:

An exotic module is a ReScript module prefixed by a ~ character

Note: We can bikeshed what the prefix is

Additionally, we should formalise the properties of an exotic module:

  • Everything after the exotic module prefix is used, un-transformed, as the resulting JS file name
  • They are invisible to other modules ie. cannot be imported or accessed by other modules
  • Exotic module file names are allowed to be duplicated across the file structure

Form a developer’s perspective these rules are much easier to understand and solve all of the problems outlined above.


References:

I’d love to hear thoughts, especially around if this is technically feasible!

9 Likes

I’d be very happy if this could be solved in a generic way once and for all as proposed here. It’d make it possible to start integrating more tightly with some of the file-system based things the JavaScript ecosystem is working on without adding more boilerplate code .mjs files that only re-export ReScript compiled code.

To open the bikeshed about the prefix I would like to request we find an alternative for ~ as it’s commonly used in filesystems for two purposes that might individually trip up other applications:

  1. Provide a shortcut to the user’s home directory
  2. Indicate a temporary file

I wonder if it would be possible to set this as some kind of keyword inside the file instead of having to rely on the file name (similar to an interpreter line on Linux like #!/usr/env bash) although I realise this makes it less visible for developers.

Alternatively would a new extension prove helpful here? .resex for example for ReScript Executable (i.e. not a module). Or .resx for ReScript eXternal (or ReScript exotic).

1 Like

Im not in love with the tilde prefix to be honest, but I do think a filename convention of some kind is the way to go instead of something inside the file itself.

The different filename makes it easy to see at a glance what you can and can’t import.

Your suggestion about a different file extension is a great one and matches the conventions in JS with ESM Vs CJS. The only hangup I have is that this would mean ReScript would have 4 different file types when you include the interface files, although I suppose exotic interface files are completely redundant.

1 Like

The only hangup I have is that this would mean ReScript would have 4 different file types when you include the interface files, although I suppose exotic interface files are completely redundant.

Yeah fair point! I think in my mind an interface either wasn’t needed or we could still use .resi. I do think it’s still important to be able to control what is exported out of the JavaScript module and what isn’t. So for that an interface file would still be used.

The bigger question may be, how does the compiler handle NotExotic.res and NotExotic.resx conflicts? I suppose in different folders they could be exist because NotExotic.res would have the current behaviour and NotExotic.resx would be an opaque module with no exports on the ReScript side. However, what if those are in the same folder? Since they would share the same JavaScript filename this would cause issues.

1 Like

The clashing if exotic and non-exotic modules isn’t something I’ve considered but it’s a pretty big hole in the model I think. It’s a great argument for a non-file-system based exotic indicator

Looping back on this, if I’m being pragmatic I don’t think this is actually a big deal.

I feel like it’s sensible to expect to have to rename the non-exotic module to be a different name.

I’ve come to realise the problem this proposal addresses is one of the biggest blockers I’ve had in selling ReScript to ors using JS and TS.

First class support for the Next.js App Router could be a huge boost to convincing folks to adopt ReScript.

Have you consider codegen? I think it’ll be much better, typesafe and more ideomatic solution

To generate the file based thingy from a config like this https://github.com/zth/rescript-relay-router/blob/main/packages/rescript-relay-router/README.md#route-json-files

Lack of file system based routing is essentially a blocker for ReScript adoption in many teams. There are of course other solutions to routing, but the JS ecosystem appears to be standardising on the Next.js-like approach. ReScript lacking support for this is hurting adoption.

Additionally, codegen is a specific solution for routing whereas this proposal is more general. It’s a way to decouple module names and file names when you need it and so has broader interop applications outside of routing.

This sounds like “JavaScript is as flexible as it can be, so ReScript is hurting adoption because is not as flexible as JavaScript”.

File-based routing is coupled with the limitations of the module system in JS, those might not apply to ReScript. In fact, there are many solutions that could give the both worlds in peace:

Provide a syntax (at the module level) that represents the file-routing syntax (with [id], and any combination) but keeping the file-name intact. That way your file-system will still be searchable and not everything will become /home/index.res (which is not possible in ReScript).

2 Likes