61
61
import java .io .IOException ;
62
62
import java .net .URI ;
63
63
import java .net .URISyntaxException ;
64
+ import java .util .HashMap ;
65
+ import java .util .LinkedList ;
64
66
import java .util .List ;
67
+ import java .util .Map ;
65
68
66
69
import com .oracle .js .parser .ir .Module .ModuleRequest ;
67
70
import com .oracle .truffle .api .CompilerDirectives .TruffleBoundary ;
@@ -98,9 +101,31 @@ public final class NpmCompatibleESModuleLoader extends DefaultESModuleLoader {
98
101
private static final String INVALID_MODULE_SPECIFIER = "Invalid module specifier: '" ;
99
102
private static final String UNSUPPORTED_FILE_EXTENSION = "Unsupported file extension: '" ;
100
103
private static final String UNSUPPORTED_PACKAGE_EXPORTS = "Unsupported package exports: '" ;
104
+ private static final String INVALID_PACKAGE_EXPORT = "Invalid package export: " ;
101
105
private static final String UNSUPPORTED_PACKAGE_IMPORTS = "Unsupported package imports: '" ;
102
106
private static final String UNSUPPORTED_DIRECTORY_IMPORT = "Unsupported directory import: '" ;
103
107
private static final String INVALID_PACKAGE_CONFIGURATION = "Invalid package configuration: '" ;
108
+ private static final String EXPORT_TYPE_GRAALJS = "graaljs" ;
109
+ private static final String EXPORT_TYPE_IMPORT = "import" ;
110
+ private static final String EXPORT_TYPE_REQUIRE = "require" ;
111
+ private static final String EXPORT_TYPE_DEFAULT = "default" ;
112
+ private static final LinkedList <String > EXPORT_TYPES ;
113
+
114
+ static {
115
+ EXPORT_TYPES = new LinkedList <>(
116
+ List .of (EXPORT_TYPE_GRAALJS , EXPORT_TYPE_IMPORT , EXPORT_TYPE_REQUIRE , EXPORT_TYPE_DEFAULT )
117
+ );
118
+ }
119
+
120
+ public static void registerPreferredExportType (String exportType ) {
121
+ if (!EXPORT_TYPES .contains (exportType )) {
122
+ EXPORT_TYPES .addFirst (exportType );
123
+ }
124
+ }
125
+
126
+ public static List <String > getRegisteredExportTypes () {
127
+ return List .copyOf (EXPORT_TYPES );
128
+ }
104
129
105
130
public static NpmCompatibleESModuleLoader create (JSRealm realm ) {
106
131
return new NpmCompatibleESModuleLoader (realm );
@@ -340,6 +365,10 @@ private Format esmFileFormat(URI url, TruffleLanguage.Env env) {
340
365
if (url .getPath ().endsWith (JS_EXT )) {
341
366
return Format .ESM ;
342
367
}
368
+ } else if (url .getPath ().endsWith (JS_EXT )) {
369
+ // Np Fallback to CJS as below (in the case that there is a package.json without a "type" field, or
370
+ // the "type" field is not "module").
371
+ return Format .CommonJS ;
343
372
}
344
373
} else if (url .getPath ().endsWith (JS_EXT )) {
345
374
// Np Package.json with .js extension: try loading as CJS like Node.js does.
@@ -349,6 +378,27 @@ private Format esmFileFormat(URI url, TruffleLanguage.Env env) {
349
378
throw fail (UNSUPPORTED_FILE_EXTENSION , url .toString ());
350
379
}
351
380
381
+ private URI exportForImport (URI packageUrl , Map <String , String > exports , TruffleLanguage .Env env ) {
382
+ // in order of preference, find the best import to use for this circumstance; this will be `graaljs` if
383
+ // specified (as top preference), then `import`, then `require`, then `default`. if the developer has registered
384
+ // their own preferred export types, these will be preferred first.
385
+ //
386
+ // this branch only activates if package exports are present and need to be used to resolve an import. thus,
387
+ // there is no fallback behavior waiting for us, and so an exception is thrown if no export can be matched.
388
+
389
+ // 1. for preferred export types...
390
+ for (String preferred : getRegisteredExportTypes ()) {
391
+ // 1.1: is it specified within the exports?
392
+ if (exports .containsKey (preferred )) {
393
+ // 1.2: if so, resolve the import from the package root. make sure to slice off the `./` prefix.
394
+ return packageUrl .resolve (exports .get (preferred ).substring (2 ));
395
+ }
396
+ }
397
+
398
+ // 2. if no preferred export types are specified, or none of them are found, throw an exception.
399
+ throw failMessage (UNSUPPORTED_PACKAGE_EXPORTS );
400
+ }
401
+
352
402
/**
353
403
* PACKAGE_RESOLVE(packageSpecifier, parentURL).
354
404
*/
@@ -424,6 +474,12 @@ private URI packageResolve(String packageSpecifier, URI parentURL, TruffleLangua
424
474
PackageJson pjson = readPackageJson (packageUrl , env );
425
475
// 11.5 If pjson is not null and pjson.exports is not null or undefined, then
426
476
if (pjson != null && pjson .hasExportsProperty ()) {
477
+ var exp = pjson .getExport (packageSubpath );
478
+ if (exp != null ) {
479
+ // we should receive a map of the form `type => path` for the requested export. determine the best
480
+ // import type to use and resolve from there.
481
+ return exportForImport (packageUrl , exp , env );
482
+ }
427
483
throw fail (UNSUPPORTED_PACKAGE_EXPORTS , packageSpecifier );
428
484
} else if (packageSubpath .equals (DOT )) {
429
485
// 11.6 Otherwise, if packageSubpath is equal to ".", then
@@ -544,6 +600,54 @@ public boolean hasExportsProperty() {
544
600
return hasNonNullProperty (jsonObj , EXPORTS_PROPERTY_NAME );
545
601
}
546
602
603
+ public Map <String , String > getExport (String specifier ) {
604
+ assert hasNonNullProperty (jsonObj , EXPORTS_PROPERTY_NAME );
605
+ var data = JSObject .get (jsonObj , EXPORTS_PROPERTY_NAME );
606
+ if (data instanceof JSDynamicObject exportsObj ) {
607
+ for (TruffleString key : JSObject .enumerableOwnNames (exportsObj )) {
608
+ // find a match for the requested export...
609
+ if (key .toString ().equals (specifier )) {
610
+ // if we found it, it should be a nested object with export mappings. at this point, we've
611
+ // already matched the path, so these are mappings of (type => path). `path` must be relative to
612
+ // the package root, must start with `.`, must not contain relative backwards references, and
613
+ // must be an extant regular file.
614
+ Object value = JSObject .get (exportsObj , key );
615
+ if (value instanceof JSDynamicObject valueObj ) {
616
+ var exportKeys = valueObj .ownPropertyKeys ();
617
+ var exportMap = new HashMap <String , String >();
618
+ for (Object exportKey : exportKeys ) {
619
+ if (exportKey instanceof TruffleString exportKeyStr ) {
620
+ Object exportValue = JSObject .get (valueObj , exportKeyStr );
621
+ if (Strings .isTString (exportValue )) {
622
+ var exportStr = exportKeyStr .toString ();
623
+ var exportVal = exportValue .toString ();
624
+ if (!exportVal .startsWith ("." ) || exportVal .contains (".." )) {
625
+ // must start with `.`, must not contain `..`
626
+ throw failMessage (INVALID_PACKAGE_EXPORT + exportStr );
627
+ }
628
+ exportMap .put (exportKeyStr .toString (), exportValue .toString ());
629
+ }
630
+ } else {
631
+ throw failMessage (UNSUPPORTED_PACKAGE_EXPORTS + exportKey .toString ());
632
+ }
633
+ }
634
+ return exportMap ;
635
+ } else if (value instanceof TruffleString exportStr ) {
636
+ // if the export is a string, it should be a path to the file to import.
637
+ if (!exportStr .toString ().startsWith ("." ) || exportStr .toString ().contains (".." )) {
638
+ // must start with `.`, must not contain `..`
639
+ throw failMessage (INVALID_PACKAGE_EXPORT + exportStr );
640
+ }
641
+ return Map .of (EXPORT_TYPE_DEFAULT , exportStr .toString ());
642
+ } else {
643
+ throw failMessage (INVALID_PACKAGE_EXPORT + value );
644
+ }
645
+ }
646
+ }
647
+ }
648
+ return null ;
649
+ }
650
+
547
651
public boolean hasMainProperty () {
548
652
if (JSObject .hasProperty (jsonObj , PACKAGE_JSON_MAIN_PROPERTY_NAME )) {
549
653
Object value = JSObject .get (jsonObj , PACKAGE_JSON_MAIN_PROPERTY_NAME );
0 commit comments