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

Bindings where `@scope` has same name as module can result in runtime errors

Open mediremi opened this issue 8 months ago • 11 comments

When a binding's @scope has the same name as a module, the binding's scope is shadowed by the module:

module URL = {  
  @val @scope("URL")
  external revokeObjectURL: string => unit = "revokeObjectURL"
}

URL.revokeObjectURL("some url")
let URL = {};

URL.revokeObjectURL("some url");

mediremi avatar May 16 '25 21:05 mediremi

I think this is not a v12 issue per se, it's just that v12 doesn't escape URL with $$ anymore.

If the module name is different, the same problem exists in v11:

module URRL = {  
  @val @scope("URRL")
  external revokeObjectURL: string => unit = "revokeObjectURL"
}

URRL.revokeObjectURL("some url")

https://rescript-lang.org/try?version=v11.1.4&module=esmodule&code=LYewJgrgNgpgBAVQEpIDJwLxwN5zgKDwAEA3AQyjiIGcBjEABxgAoAiZNVgSkLhgA8ALjABOAOwpwRMEiADWMAPIAjAFYxag5KgBccaoJEBLMQHNMAPjgQxRwZjitpshSvWbtrfAF98+DqgAdM7ySmoaWmhs1CDA8BAiUNz4QA

cknitt avatar May 17 '25 08:05 cknitt

Ah I didn't read the breaking changes section of v12.0.0-alpha.1 carefully enough - I see now that since https://github.com/rescript-lang/rescript/pull/6831 we're not escaping browser globals anymore.

I'll update this issue title and description to not be v12 specific 👍

mediremi avatar May 17 '25 18:05 mediremi

The problem is not because of @scope, but the module name.

cometkim avatar May 17 '25 21:05 cometkim

I see. It should be something like:

let URL$1 = {};

URL.revokeObjectURL("some url");

export {
  URL$1 as URL,
}

cometkim avatar May 18 '25 09:05 cometkim

Workaround (IMO the correct resolution):

module URL = {  
  @val @scope("globalThis.URL")
  external revokeObjectURL: string => unit = "revokeObjectURL"
}

URL.revokeObjectURL("some url")

cometkim avatar May 18 '25 09:05 cometkim

Although this can be broken in basically the same way:

module URL = {  
  @val @scope("globalThis.URL")
  external revokeObjectURL: string => unit = "revokeObjectURL"
}

let globalThis = 0

URL.revokeObjectURL("some url")

cknitt avatar May 19 '25 11:05 cknitt

globalThis should be mangled by default.

The output in v12 is:

let URL = {};

globalThis.URL.revokeObjectURL("some url");

let $$globalThis = 0;

export {
  URL,
  $$globalThis,
}
/*  Not a pure module */

cometkim avatar May 19 '25 13:05 cometkim

One downside of leveraging globalThis is that it doesn't look handwritten. But I guess there's no other good way around this.

zth avatar May 20 '25 06:05 zth

Maybe we can make it as a standard like @globalScope(..) ?

cometkim avatar May 20 '25 06:05 cometkim

Are there any alternative ways around this? Could we detect when this unintended clash happens and only then emit globalThis?

zth avatar May 20 '25 06:05 zth

globalThis should be mangled by default.

Ah, right, it is mangled in v12, but not v11.

cknitt avatar May 20 '25 07:05 cknitt