rescript-compiler icon indicating copy to clipboard operation
rescript-compiler copied to clipboard

Proof of concept: one shot file compilation

Open nojaf opened this issue 1 month ago • 3 comments

Use case: I want to compile a single ReScript file and run it immediately. With this PR I could do rescript compile-file MyFile.res | bun - and bun automatically runs it.

(Would not be a stretch to create a Bun plugin and just be able to run bun run MyFile.res if plugin is configured)

Going to keep this as draft until after v12.

nojaf avatar Nov 04 '25 08:11 nojaf

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@8002
@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@8002
@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@8002
@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@8002
@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@8002
@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@8002
@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@8002

commit: 102f806

pkg-pr-new[bot] avatar Nov 04 '25 08:11 pkg-pr-new[bot]

Split bsc module system flags for stdout compilation

Problem

When bsc compiles to stdout (no -bs-package-output specified), it hardcoded the Commonjs module system:

(* compiler/core/lam_compile_main.ml:295 - old code *)
if Js_packages_info.is_empty package_info && !Js_config.js_stdout then
  Js_dump_program.dump_deps_program ~output_prefix Commonjs (* <- hardcoded! *)

This meant stdout output was always Commonjs, regardless of project configuration. Since the new compile-file command outputs to stdout, it could only generate Commonjs, not ES6 modules.

Solution

Split -bs-package-output into three independent flags:

Before (coupled):

-bs-package-output esmodule:lib/es6:.mjs
# Format: module_system:path:suffix (all three or none)

After (decoupled):

-bs-module-system esmodule    # Controls import/export syntax
-bs-suffix .mjs              # Controls import path extensions  
-bs-package-output lib/es6   # Output directory (optional)

Implementation

New compiler state (compiler/common/js_config.ml):

let default_module_system = ref Ext_module_system.Commonjs
let default_suffix = ref Literals.suffix_js

New flags (compiler/bsc/rescript_compiler_main.ml):

("-bs-module-system", string_call set_module_system, "Set module system: commonjs, esmodule, es6-global");
("-bs-suffix", string_call set_suffix, "Set import file suffix: .js, .mjs, .cjs");

Stdout output now uses configured values (compiler/core/lam_compile_main.ml):

if Js_packages_info.is_empty package_info && !Js_config.js_stdout then
  (* Use configured module system instead of hardcoded Commonjs *)
  Js_dump_program.dump_deps_program ~output_prefix !Js_config.default_module_system

Import path generation uses configured suffix (compiler/core/js_name_of_module_id.ml):

(* For Package_script mode (stdout), use configured suffix *)
let js_file = Ext_namespace.js_name_of_modulename dep_module_id.id.name case !Js_config.default_suffix

Critical exception: Runtime package imports (@rescript/runtime) always use .js because the runtime is pre-compiled and distributed with .js files:

(* Runtime package is pre-compiled and always uses .js suffix *)
let js_file = Ext_namespace.js_name_of_modulename dep_module_id.id.name Upper Literals.suffix_js

Build System Updates

Rewatch (rewatch/src/build/compile.rs):

// Old format:
"-bs-package-output", "esmodule:lib/es6:.mjs"

// New format:
"-bs-module-system", "esmodule",
"-bs-suffix", ".mjs", 
"-bs-package-output", "lib/es6"

Legacy bsb (compiler/bsb/bsb_package_specs.ml):

(* Old: single compound flag *)
"-bs-package-output esmodule:lib/es6:.mjs"

(* New: three separate flags *)
"-bs-module-system esmodule -bs-suffix .mjs -bs-package-output lib/es6"

Backward Compatibility

The -bs-package-output parser still accepts the old format:

  • module:path:suffix - all three explicit (old format)
  • module:path - uses configured suffix
  • path - uses configured module system and suffix

This allows gradual migration, though since these are internal flags (users don't call bsc directly), backward compatibility isn't critical.

Result

bsc can now output ES6 modules to stdout:

$ bsc -bs-module-system esmodule -bs-suffix .mjs file.ast

// Generated by ReScript, PLEASE EDIT WITH CARE

import * as MyDep from "./MyDep.mjs";
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";

export { ... }

Notice:

  • User module imports: .mjs (from -bs-suffix)
  • Runtime imports: .js (always, because runtime is pre-compiled)

This enables compile-file to respect project configuration when outputting to stdout.

//cc @cristianoc , @zth

nojaf avatar Nov 04 '25 16:11 nojaf

Hi @cristianoc , the splitting of the bsc arg turned out quite ugly. To satisfy bsb, a lot of code was generated while it seems a lot more straightforward in Rewatch. Any pointers on what could be improved?

nojaf avatar Nov 05 '25 07:11 nojaf