From 2ae8c9919fdcd9bd03b618e4fef2730c037b078a Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 19 Oct 2026 19:50:48 +0000 Subject: [PATCH] Import the default export for @scope starting with "default" A `@module` external whose `@scope` starts with `"default"` now imports the module's default export, as `external x: t = "default"` already does, and accesses the remaining scopes on it. Under ES modules this emits `import Mod from "mod"` instead of a namespace import plus `.default`; CommonJS output becomes `require("mod").default`. Other scopes are unchanged. Example (rescript): @module("mod2") @scope(("default", "x")) external fn: int => unit = "fn" let f = () => fn(1) Before (js): import * as Mod2 from "mod2"; function f() { Mod2.default.x.fn(1); } After (js): import Mod2 from "mod2"; function f() { Mod2.x.fn(1); } Source: Before from the issue's playground example on master 58da277de4; After from tests/tests/src/scope_default_import.mjs in the diff. Review: Does CommonJS stay equivalent? Yes: `Mod1.default.x.fn` with `Mod1 = require("mod1")` becomes `Mod1$1.x.fn` with `Mod1$1 = require("mod1").default`, the same lowering `= "default"` uses (tests/commonjs_tests/src/scope_default_import.js). Area: codegen tests Output: changed programs with @module externals whose @scope starts with "default"; no existing generated JS in the repository changes Tests: +tests/tests/src/scope_default_import.res +tests/commonjs_tests/src/scope_default_import.res Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- changes/scope-default-import.polish.md | 1 + compiler/core/lam_compile_external_call.ml | 5 +++-- .../src/scope_default_import.js | 18 ++++++++++++++++++ .../src/scope_default_import.res | 11 +++++++++++ tests/tests/src/scope_default_import.mjs | 19 +++++++++++++++++++ tests/tests/src/scope_default_import.res | 11 +++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 changes/scope-default-import.polish.md create mode 100644 tests/commonjs_tests/src/scope_default_import.js create mode 100644 tests/commonjs_tests/src/scope_default_import.res create mode 100644 tests/tests/src/scope_default_import.mjs create mode 100644 tests/tests/src/scope_default_import.res diff --git a/changes/scope-default-import.polish.md b/changes/scope-default-import.polish.md new file mode 100644 index 00000000000..0ecb1e43925 --- /dev/null +++ b/changes/scope-default-import.polish.md @@ -0,0 +1 @@ +- Import the default export, instead of the module namespace, for `@module` externals whose `@scope` starts with `"default"`. diff --git a/compiler/core/lam_compile_external_call.ml b/compiler/core/lam_compile_external_call.ml index d0449bd32c9..b2f01a8fc29 100644 --- a/compiler/core/lam_compile_external_call.ml +++ b/compiler/core/lam_compile_external_call.ml @@ -223,8 +223,9 @@ let translate_scoped_module_val E.external_var_field ?import_attributes ~external_name:bundle ~field:fn ~default id | x :: rest -> - (* TODO: what happens when scope contains "default" ?*) - let default = false in + (* A leading "default" scope is the module's default export: import + it as such, like [external x: t = "default"] *) + let default = x = "default" in let id = Lam_compile_env.add_js_module ?import_attributes module_bind_name bundle default ~dynamic_import:false diff --git a/tests/commonjs_tests/src/scope_default_import.js b/tests/commonjs_tests/src/scope_default_import.js new file mode 100644 index 00000000000..890f7060a77 --- /dev/null +++ b/tests/commonjs_tests/src/scope_default_import.js @@ -0,0 +1,18 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE +'use strict'; + +let Mod1 = require("mod1"); +let Mod1$1 = require("mod1").default; +let Mod2 = require("mod2").default; + +function f() { + Mod1$1.x.fn((Mod1$1.value + Mod1.other.other | 0) + Mod2.default | 0); +} + +function g() { + return new (Mod1$1.Klass)(); +} + +exports.f = f; +exports.g = g; +/* mod1 Not a pure module */ diff --git a/tests/commonjs_tests/src/scope_default_import.res b/tests/commonjs_tests/src/scope_default_import.res new file mode 100644 index 00000000000..43de8427618 --- /dev/null +++ b/tests/commonjs_tests/src/scope_default_import.res @@ -0,0 +1,11 @@ +// A leading "default" scope imports the module's default export + +@module("mod1") @scope(("default", "x")) external fn: int => unit = "fn" +@module("mod1") @scope("default") external value: int = "value" +@module("mod1") @new @scope("default") external make: unit => {"a": int} = "Klass" +@module("mod1") @scope("other") external other: int = "other" + +@module("mod2") @scope("default") external defaultOfDefault: int = "default" + +let f = () => fn(value + other + defaultOfDefault) +let g = () => make() diff --git a/tests/tests/src/scope_default_import.mjs b/tests/tests/src/scope_default_import.mjs new file mode 100644 index 00000000000..593b7936410 --- /dev/null +++ b/tests/tests/src/scope_default_import.mjs @@ -0,0 +1,19 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mod1 from "mod1"; +import Mod1$1 from "mod1"; +import Mod2 from "mod2"; + +function f() { + Mod1$1.x.fn((Mod1$1.value + Mod1.other.other | 0) + Mod2.default | 0); +} + +function g() { + return new (Mod1$1.Klass)(); +} + +export { + f, + g, +} +/* mod1 Not a pure module */ diff --git a/tests/tests/src/scope_default_import.res b/tests/tests/src/scope_default_import.res new file mode 100644 index 00000000000..43de8427618 --- /dev/null +++ b/tests/tests/src/scope_default_import.res @@ -0,0 +1,11 @@ +// A leading "default" scope imports the module's default export + +@module("mod1") @scope(("default", "x")) external fn: int => unit = "fn" +@module("mod1") @scope("default") external value: int = "value" +@module("mod1") @new @scope("default") external make: unit => {"a": int} = "Klass" +@module("mod1") @scope("other") external other: int = "other" + +@module("mod2") @scope("default") external defaultOfDefault: int = "default" + +let f = () => fn(value + other + defaultOfDefault) +let g = () => make()