Skip to content

Commit 7c8a9a0

Browse files
committed
Add ability to install custom JS module loaders.
- Adds `JSModuleLoaderFactory` interface for ESM loader hook. - Adds `CommonJSResolverHook` interface for CJS resolver hook. - Adds `js.module-loader-factory=handler` setting to enable. - Adjusts `JSEngine` to retain the installed factory and/or resolver. - Adjusts `JSRealm` to use `JSEngine` to create the module loader. - Adjusts `NpmCompatibleESModuleLoader` to be extensible. - Adjusts `CommonJSResolution` to use the resolver hook if present. Relates to oracle/graal#9177 Signed-off-by: Sam Gammon <sam@elide.dev>
1 parent 69e44ba commit 7c8a9a0

File tree

8 files changed

+221
-7
lines changed

8 files changed

+221
-7
lines changed

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/CommonJSRequireBuiltin.java

+17
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.MJS_EXT;
4747
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.NODE_EXT;
4848
import static com.oracle.truffle.js.builtins.commonjs.CommonJSResolution.getCoreModuleReplacement;
49+
import static com.oracle.truffle.js.runtime.JSContextOptions.ModuleLoaderFactoryMode.HANDLER;
4950

5051
import java.util.Map;
5152
import java.util.Objects;
@@ -64,6 +65,7 @@
6465
import com.oracle.truffle.js.runtime.Errors;
6566
import com.oracle.truffle.js.runtime.JSArguments;
6667
import com.oracle.truffle.js.runtime.JSContext;
68+
import com.oracle.truffle.js.runtime.JSEngine;
6769
import com.oracle.truffle.js.runtime.JSErrorType;
6870
import com.oracle.truffle.js.runtime.JSException;
6971
import com.oracle.truffle.js.runtime.JSRealm;
@@ -169,6 +171,21 @@ protected static Object fallback(@SuppressWarnings("unused") Object function, Ob
169171
@TruffleBoundary
170172
private Object requireImpl(String moduleIdentifier, TruffleFile entryPath, JSRealm realm) {
171173
log("required module '", moduleIdentifier, "' from path ", entryPath);
174+
// 1.1 (Non-spec): If a module resolver hook has been installed, give it a chance to resolve the module, but
175+
// only if `handler` mode is enabled for JS module resolution.
176+
if (realm.getContextOptions().getModuleLoaderFactoryMode().equals(HANDLER)) {
177+
var resolver = JSEngine.getCjsResolverHook();
178+
if (resolver != null) {
179+
log("custom import hook is active; there is a resolver. loading module '", moduleIdentifier, "'");
180+
var maybeResolved = resolver.resolveModule(realm, moduleIdentifier, entryPath);
181+
if (maybeResolved != null) {
182+
log("custom handler returned module impl for '", moduleIdentifier, "'");
183+
return maybeResolved;
184+
} else if (LOG_REQUIRE_PATH_RESOLUTION) {
185+
log("custom handler returned null; falling back for module '", moduleIdentifier, "'");
186+
}
187+
}
188+
}
172189
String moduleReplacementName = getCoreModuleReplacement(realm, moduleIdentifier);
173190
if (moduleReplacementName != null) {
174191
log("using module replacement for module '", moduleIdentifier, "' with ", moduleReplacementName);

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/CommonJSResolution.java

+2
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@
5959
import com.oracle.truffle.js.lang.JavaScriptLanguage;
6060
import com.oracle.truffle.js.runtime.Errors;
6161
import com.oracle.truffle.js.runtime.JSArguments;
62+
import com.oracle.truffle.js.runtime.JSEngine;
6263
import com.oracle.truffle.js.runtime.JSRealm;
6364
import com.oracle.truffle.js.runtime.Strings;
6465
import com.oracle.truffle.js.runtime.builtins.JSFunction;
6566
import com.oracle.truffle.js.runtime.builtins.JSFunctionObject;
6667
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
6768
import com.oracle.truffle.js.runtime.objects.JSObject;
6869
import com.oracle.truffle.js.runtime.objects.Undefined;
70+
import static com.oracle.truffle.js.runtime.JSContextOptions.ModuleLoaderFactoryMode.HANDLER;
6971

7072
public final class CommonJSResolution {
7173

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/NpmCompatibleESModuleLoader.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
import com.oracle.truffle.js.runtime.objects.ScriptOrModule;
8888
import com.oracle.truffle.js.runtime.objects.Undefined;
8989

90-
public final class NpmCompatibleESModuleLoader extends DefaultESModuleLoader {
90+
public class NpmCompatibleESModuleLoader extends DefaultESModuleLoader {
9191

9292
private static final URI TryCommonJS = URI.create("custom:///try-common-js-token");
9393
private static final URI TryCustomESM = URI.create("custom:///try-custom-esm-token");
@@ -106,7 +106,7 @@ public static NpmCompatibleESModuleLoader create(JSRealm realm) {
106106
return new NpmCompatibleESModuleLoader(realm);
107107
}
108108

109-
private NpmCompatibleESModuleLoader(JSRealm realm) {
109+
protected NpmCompatibleESModuleLoader(JSRealm realm) {
110110
super(realm);
111111
}
112112

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.truffle.js.runtime;
42+
43+
import com.oracle.truffle.api.TruffleFile;
44+
45+
/**
46+
* Allows GraalJs users to hook into the JavaScript CJS loading process.
47+
*/
48+
public interface CommonJSResolverHook {
49+
/**
50+
* Resolve a CommonJS module identifier to a file.
51+
*
52+
* <p>Return types which are valid include:
53+
* <ul>
54+
* <li>{@link TruffleFile}: Will be interpreted as normal (i.e. as a CJS file)</li>
55+
* <li>Guest-compatible value: Returned as the module itself, without evaluation</li>
56+
* </ul>
57+
* </p>
58+
*
59+
* @param realm the realm in which the module is being resolved
60+
* @param moduleIdentifier the CommonJS module identifier
61+
* @param entryPath the path of the module that is importing the module
62+
* @return Optional wrapping the type which should be returned for this module implementation; supported types
63+
* are listed above.
64+
*/
65+
Object resolveModule(JSRealm realm, String moduleIdentifier, TruffleFile entryPath);
66+
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContextOptions.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,25 @@ public String toString() {
638638
public static final OptionKey<UnhandledRejectionsTrackingMode> UNHANDLED_REJECTIONS = new OptionKey<>(UnhandledRejectionsTrackingMode.NONE);
639639
@CompilationFinal private UnhandledRejectionsTrackingMode unhandledRejectionsMode;
640640

641+
public enum ModuleLoaderFactoryMode {
642+
DEFAULT,
643+
HANDLER;
644+
645+
@Override
646+
public String toString() {
647+
return name().toLowerCase(Locale.ENGLISH);
648+
}
649+
}
650+
651+
public static final String MODULE_LOADER_FACTORY_NAME = JS_OPTION_PREFIX + "module-loader-factory";
652+
653+
@Option(name = MODULE_LOADER_FACTORY_NAME, category = OptionCategory.USER, stability = OptionStability.EXPERIMENTAL, sandbox = SandboxPolicy.TRUSTED, help = """
654+
Configure a factory for overriding the JavaScript module loader. Accepted values: \
655+
'default', default behavior applies for CommonJS and ESM. \
656+
'handler', the handler function set with JSEngine.setModuleLoaderFactory will be called to satisfy CJS or ESM imports.""") //
657+
public static final OptionKey<ModuleLoaderFactoryMode> MODULE_LOADER_FACTORY_MODE = new OptionKey<>(ModuleLoaderFactoryMode.DEFAULT);
658+
@CompilationFinal private ModuleLoaderFactoryMode moduleLoaderFactoryMode;
659+
641660
public static final String OPERATOR_OVERLOADING_NAME = JS_OPTION_PREFIX + "operator-overloading";
642661
@Option(name = OPERATOR_OVERLOADING_NAME, category = OptionCategory.USER, help = "Enable operator overloading") //
643662
public static final OptionKey<Boolean> OPERATOR_OVERLOADING = new OptionKey<>(false);
@@ -810,7 +829,8 @@ private void cacheOptions(SandboxPolicy sandboxPolicy) {
810829
this.useUTCForLegacyDates = USE_UTC_FOR_LEGACY_DATES.hasBeenSet(optionValues) ? readBooleanOption(USE_UTC_FOR_LEGACY_DATES) : !v8CompatibilityMode;
811830
this.webAssembly = readBooleanOption(WEBASSEMBLY);
812831
this.unhandledRejectionsMode = readUnhandledRejectionsMode();
813-
this.newSetMethods = readBooleanOption(NEW_SET_METHODS, JSConfig.ECMAScript2025);
832+
this.moduleLoaderFactoryMode = readModuleLoaderFactoryMode();
833+
this.newSetMethods = readBooleanOption(NEW_SET_METHODS, JSConfig.ECMAScript2025);
814834
this.atomicsWaitAsync = readBooleanOption(ATOMICS_WAIT_ASYNC, JSConfig.ECMAScript2024);
815835
this.asyncIteratorHelpers = getEcmaScriptVersion() >= JSConfig.ECMAScript2018 && readBooleanOption(ASYNC_ITERATOR_HELPERS);
816836
this.iteratorHelpers = getEcmaScriptVersion() >= JSConfig.ECMAScript2018 && (this.asyncIteratorHelpers || readBooleanOption(ITERATOR_HELPERS, JSConfig.ECMAScript2025));
@@ -845,6 +865,10 @@ private UnhandledRejectionsTrackingMode readUnhandledRejectionsMode() {
845865
return UNHANDLED_REJECTIONS.getValue(optionValues);
846866
}
847867

868+
private ModuleLoaderFactoryMode readModuleLoaderFactoryMode() {
869+
return MODULE_LOADER_FACTORY_MODE.getValue(optionValues);
870+
}
871+
848872
private boolean readBooleanOption(OptionKey<Boolean> key) {
849873
return key.getValue(optionValues);
850874
}
@@ -1336,4 +1360,7 @@ public boolean isWorker() {
13361360
return worker;
13371361
}
13381362

1363+
public ModuleLoaderFactoryMode getModuleLoaderFactoryMode() {
1364+
return moduleLoaderFactoryMode;
1365+
}
13391366
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSEngine.java

+24
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,22 @@
4040
*/
4141
package com.oracle.truffle.js.runtime;
4242

43+
import java.util.Optional;
4344
import java.util.ServiceLoader;
4445

46+
import com.oracle.truffle.api.CompilerDirectives;
4547
import com.oracle.truffle.api.TruffleLanguage;
4648
import com.oracle.truffle.js.lang.JavaScriptLanguage;
4749

4850
public final class JSEngine {
4951
private static final JSEngine INSTANCE = new JSEngine();
5052

53+
@CompilerDirectives.CompilationFinal
54+
private static volatile JSModuleLoaderFactory MODULE_LOADER_FACTORY = null;
55+
56+
@CompilerDirectives.CompilationFinal
57+
private static volatile CommonJSResolverHook CJS_RESOLVER_HOOK = null;
58+
5159
private final Evaluator parser;
5260

5361
private JSEngine() {
@@ -71,6 +79,22 @@ private JSContext createContext(JavaScriptLanguage language, TruffleLanguage.Env
7179
return JSContext.createContext(parser, language, env);
7280
}
7381

82+
public static void setModuleLoaderFactory(JSModuleLoaderFactory factory) {
83+
MODULE_LOADER_FACTORY = factory;
84+
}
85+
86+
public static void setCjsResolverHook(CommonJSResolverHook hook) {
87+
CJS_RESOLVER_HOOK = hook;
88+
}
89+
90+
public static JSModuleLoaderFactory getModuleLoaderFactory() {
91+
return MODULE_LOADER_FACTORY;
92+
}
93+
94+
public static CommonJSResolverHook getCjsResolverHook() {
95+
return CJS_RESOLVER_HOOK;
96+
}
97+
7498
public static JSContext createJSContext(JavaScriptLanguage language, TruffleLanguage.Env env) {
7599
return JSEngine.getInstance().createContext(language, env);
76100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.truffle.js.runtime;
42+
43+
import com.oracle.truffle.js.runtime.objects.JSModuleLoader;
44+
45+
/**
46+
* Allows GraalJs users to hook into the JavaScript ESM loading process.
47+
*/
48+
public interface JSModuleLoaderFactory {
49+
/**
50+
* Create an instance of the JavaScript module loader.
51+
*
52+
* @param realm JavaScript realm which intends to own this loader.
53+
* @return Loader instance.
54+
*/
55+
JSModuleLoader createLoader(JSRealm realm);
56+
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java

+26-4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.Locale;
5656
import java.util.Map;
5757
import java.util.Objects;
58+
import java.util.Optional;
5859
import java.util.SplittableRandom;
5960
import java.util.WeakHashMap;
6061

@@ -2859,12 +2860,33 @@ public JSModuleLoader getModuleLoader() {
28592860
@TruffleBoundary
28602861
private synchronized void createModuleLoader() {
28612862
if (moduleLoader == null) {
2862-
if (getContextOptions().isCommonJSRequire()) {
2863-
moduleLoader = NpmCompatibleESModuleLoader.create(this);
2864-
} else {
2865-
moduleLoader = DefaultESModuleLoader.create(this);
2863+
JSModuleLoader loader = null;
2864+
switch (getContextOptions().getModuleLoaderFactoryMode()) {
2865+
case HANDLER -> loader = loadCustomModuleLoaderOrFallBack();
2866+
case DEFAULT -> loader = createStandardModuleLoader(this);
28662867
}
2868+
assert loader != null;
2869+
moduleLoader = loader;
2870+
}
2871+
}
2872+
2873+
private JSModuleLoader loadCustomModuleLoaderOrFallBack() {
2874+
JSModuleLoaderFactory fac = JSEngine.getModuleLoaderFactory();
2875+
if (fac == null) {
2876+
return createStandardModuleLoader(this);
2877+
}
2878+
var loader = fac.createLoader(this);
2879+
if (loader == null) {
2880+
return createStandardModuleLoader(this);
2881+
}
2882+
return loader;
2883+
}
2884+
2885+
private static JSModuleLoader createStandardModuleLoader(JSRealm realm) {
2886+
if (realm.getContextOptions().isCommonJSRequire()) {
2887+
return NpmCompatibleESModuleLoader.create(realm);
28672888
}
2889+
return DefaultESModuleLoader.create(realm);
28682890
}
28692891

28702892
public final JSAgent getAgent() {

0 commit comments

Comments
 (0)