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.
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:
Provide a shortcut to the user’s home directory
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).
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.
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.
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
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).