RFC: conditional module FFI
Background
Imagine a class statement written from external JS.
// rescript_error.mjs
export class RescriptError extends Error {
constructor(exnId, payload) {
super();
}
}
// and in rescript_error.cjs
module.exports = {
RescriptError,
};
This is a pretty common situation because ReScript doesn't support ES classes.
However, ReScript's current FFI approach cannot select the appropriate resolution by its syntax.
type t
@module("./rescript_error.mjs") @new external makeErrorEsm: unit => t = "RescriptError"
@module("./rescript_error.cjs") @new external makeErrorCjs: unit => t = "RescriptError"
// and it might need a runtime like UMD
This can be solved today using bundlers' module alias or Node.js' conditional exports/imports resolution, but these are all platform-dependent features.
{
"imports": {
"#error": {
"import": "./rescript_error.mjs",
"require": "./rescript_error.cjs"
}
}
}
The toolchain must be able to produce output that is appropriate for its target. That's true even for FFIs.
Suggested Solution
There are currently two module specs: esmoudle and commonjs (a composite of the two, dual, will be added in the future #6209).
Users can specify either of these two as target in the @module statement.
type t
@module({ target: "esmodule", from: "./rescript_error.mjs" })
@module({ target: "commonjs", from: "./rescript_error.cjs" })
@new external makeError: unit => t = "RescriptError"
The library may not support all conditions. The compiler should raise an error if it cannot find a target that matches a module in the project.
If target omitted, the * pattern is implicitly used.
@module("./rescript_error.ts")
// is equivalent
@module({ target: "*", from: "./rescript_error.ts" })
Yes, that's a great catch and would be very useful. I'm all for it 👍
This is great! I am not sure I like the name "cond" though.
Maybe we should call it the same as we are calling it in rescript.json?
Maybe we should call it the same as we are calling it in rescript.json?
That's "module" (the format), already duplicated with @module keyword. And If we use the "module" here, we should use something like module_ to avoid keyword collisions.
"cond" is inspired by Node.js' and bundlers' "conditions"
- https://nodejs.org/api/packages.html#conditional-exports
- https://esbuild.github.io/api/#conditions
Some tools does something similar with "env" or "mode". It's basically a flag to customize full config.
- https://babeljs.io/docs/options#env
- https://vitejs.dev/guide/env-and-mode
Rust has "target", which is probably the most common way to express compilation conditions.
- https://doc.rust-lang.org/reference/conditional-compilation.html
The
@moduleresolution must be selected for all conditions. If omitted, the*pattern is implicitly used.
This should be relaxed based on the actual project configuration. Then implementation is a bit more complex, but it helps to maintain forward compatibility with future targets.
@zth @cknitt Does the last change make sense?
Looks good to me!
I think for the example with RescriptError what we actually need is a feature for @module to resolve relative path to the compiled ReScript output.
But the feature is also very nice and will be useful in any way
Or what about type instead of target or cond?
@module({ type: "esmodule", from: "./rescript_error.mjs" })
Personally, I don't prefer to call the "targeted module format" config as just "type"
And it might be confusing with the standard "type" attribute.
@module({ type_: "esmodule", with: { type_: "json", from: "./file.json" } })
@val external content: Json.t = "default"
Ok, and what about format?
@module({ format: "esmodule", from: "./rescript_error.mjs" })
?
Ok, and what about
format?
I think that it's good if its mental model is the format of the from source, not to tailor the compilation output.
I like target
Ok, seems people like target most. Fine with me too! 👍