diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/index.js new file mode 100644 index 00000000000..f7e0b20019c --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +module.exports.hello = '. export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/register-strip.mjs b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/register-strip.mjs new file mode 100644 index 00000000000..83bffa4d0bb --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/dist/register-strip.mjs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = './register export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/package.json new file mode 100644 index 00000000000..4db4be70040 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/basic/package.json @@ -0,0 +1,10 @@ +{ + "name": "basic", + "version": "1.0.0", + "description": "@see graal-nodejs/deps/amaro/package.json", + "type": "commonjs", + "exports": { + ".": "./dist/index.js", + "./register": "./dist/register-strip.mjs" + } +} diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/fallback.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/fallback.js new file mode 100644 index 00000000000..40c80a36df1 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/fallback.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +module.exports.hello = '. export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.js new file mode 100644 index 00000000000..52a87cc2ed6 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +module.exports.hello = '. export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.mjs b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.mjs new file mode 100644 index 00000000000..eac7dd2e678 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/dist/index.mjs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = '. export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/package.json new file mode 100644 index 00000000000..9b411d1bf21 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/package.json @@ -0,0 +1,14 @@ +{ + "name": "conditions-array", + "version": "1.0.0", + "exports": { + ".": [ + { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./dist/fallback.js" + ], + "./package.json": "./package.json" + } +} \ No newline at end of file diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/readme.md b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/readme.md new file mode 100644 index 00000000000..34d5e227a30 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-array/readme.md @@ -0,0 +1,29 @@ +In Node.js, when `"exports"` is an **array**, Node will try each entry **in +order** and use the **first one that successfully resolves**. + +In the example: + +```json +".": [ + { + "import": "./dist/walk.mjs", + "require": "./dist/walk.js" + }, + "./dist/walk.js" +] +``` + +* Node first evaluates the object `{ "import": ..., "require": ... }`: + + * If the consumer does `import`, it tries `"import"`. + * If the consumer does `require()`, it tries `"require"`. + +* If that object **cannot be resolved** for some reason (e.g., environment + doesn’t match any condition, file is missing), Node moves on to the **next + array entry** (`"./dist/walk.js"`). + +The last item acts as a **fallback** if the earlier conditional object fails. + +In modern Node with properly configured files, this fallback is usually +redundant because each condition (`import`/`require`) would normally resolve. +The array pattern mostly exists for **robustness or backward compatibility**. diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.js new file mode 100644 index 00000000000..66776c61173 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +module.exports.hello = '[entry] export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.mjs b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.mjs new file mode 100644 index 00000000000..1fecc0119a8 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/graal/index.mjs @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = '[entry] export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/node/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/node/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/node/index.mjs b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/node/index.mjs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/package.json new file mode 100644 index 00000000000..4c47b67adac --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/package.json @@ -0,0 +1,14 @@ +{ + "name": "conditions-entry", + "version": "1.0.0", + "exports": { + "graal": { + "import": "./graal/index.mjs", + "require": "./graal/index.js" + }, + "node": { + "import": "./feature.mjs", + "require": "./feature.cjs" + } + } +} \ No newline at end of file diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/readme.md b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/readme.md new file mode 100644 index 00000000000..d7fe46e9f95 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-entry/readme.md @@ -0,0 +1,31 @@ +In this example, the `"exports"` field is defining **entry points for the +package** based on runtime conditions: + +```json +"exports": { + "graal": { + "import": "./graal.mjs", + "require": "./graal.cjs" + }, + "node": { + "import": "./node.mjs", + "require": "./node.cjs" + } +} +``` + +* `"graal"` and `"node"` are **condition keys**, not subpaths. +* Node (or a supporting loader) selects the first condition that matches the + environment. +* Inside each condition, `"import"` and `"require"` define the **module system + entry point**. + +So this configuration: + +* Is **only for the package root entry**, i.e., `import 'package'` or + `require('package')`. +* Does **not define subpaths** like `"./something.js"`; it only tells Node which + file to load depending on the runtime or module system. + +If you wanted subpaths, each condition could contain objects with `"."` or +`"./sub.js"` keys instead. diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/commonjs/index.d.ts b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/commonjs/index.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/commonjs/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/commonjs/index.js new file mode 100644 index 00000000000..e1cce7a7929 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/commonjs/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = '. [default] export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/esm/index.d.ts b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/esm/index.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/esm/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/esm/index.js new file mode 100644 index 00000000000..ae93d9846cd --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/dist/esm/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = '. [default] export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/package.json new file mode 100644 index 00000000000..420f119e6d0 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/package.json @@ -0,0 +1,17 @@ +{ + "name": "conditions-nested", + "version": "1.0.0", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } + } +} \ No newline at end of file diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/readme.md b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/readme.md new file mode 100644 index 00000000000..b24495087d1 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-nested/readme.md @@ -0,0 +1,61 @@ +# Nested conditions + +[Node docs](https://nodejs.org/api/packages.html#nested-conditions) + +```json +"exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } +} +``` + +Here’s what’s actually happening: + +1. `"."` is the **export path for the main entry** (`import 'package'` or + `require('package')`). + +2. Inside `"."`, Node uses **conditional exports**: + + * `"import"` → Node picks this object **if the consumer does an ESM + `import`**. + * `"require"` → Node picks this object **if the consumer does a CommonJS + `require()`**. + +3. The objects under `"import"` and `"require"` are **not Node conditions + themselves**, they’re **sub-fields used by the package/tooling**: + + * `"default"` → the JS file to load for that system + * `"types"` → the TypeScript declarations + +So the reason for this **two-level structure** is: + +* Node only cares about the first-level `"import"`/`"require"` keys to pick the + module system. +* The inner `"default"`/`"types"` is **package-specific metadata** (TypeScript + support, or multiple entry points for tooling). + +It’s perfectly valid, but **Node itself ignores `"types"`** — it just picks +`"default"` for the resolution. + +Essentially: + +``` +"." (export path) + ├─ "import" → chosen if import() + │ ├─ "default" → actual JS + │ └─ "types" → TS definitions + └─ "require" → chosen if require() + ├─ "default" → actual JS + └─ "types" → TS definitions +``` + +Node picks the outer key (`import` vs `require`), then the package uses +`"default"`/`"types"` inside for its own purposes. diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/commonjs/test.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/commonjs/test.js new file mode 100644 index 00000000000..c12cfc93380 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/commonjs/test.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +module.exports.hello = './test export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/esm/index.d.ts b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/esm/index.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/esm/test.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/esm/test.js new file mode 100644 index 00000000000..0545ea8f850 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/dist/esm/test.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = './test export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/package.json new file mode 100644 index 00000000000..e4c85ae10ea --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/package.json @@ -0,0 +1,12 @@ +{ + "name": "conditions-path", + "version": "1.0.0", + "exports": { + "./package.json": "./package.json", + "./test": { + "import": "./dist/esm/test.js", + "require": "./dist/commonjs/test.js", + "types": "./dist/esm/index.d.ts" + } + } +} \ No newline at end of file diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/readme.md b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/readme.md new file mode 100644 index 00000000000..fa151d3402d --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/conditions-path/readme.md @@ -0,0 +1,51 @@ +# Conditional exports + +[Node docs](https://nodejs.org/api/packages.html#conditional-exports) + +```json +"exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/commonjs/index.js", + "types": "./dist/esm/index.d.ts" + } +} +``` + +## How this works + +1. **Export path `"."`** + + * Represents the **package root** (`import 'package'` or + `require('package')`). + +2. **Conditional exports at the first level inside `"."`** + + * `"import"` → Node uses this file for **ESM `import`** statements. + * `"require"` → Node uses this file for **CommonJS `require()`**. + +3. **Other fields inside the same object** + + * `"types"` → TypeScript declaration file; Node ignores this, it’s only for + tooling. + * `"default"` is no longer needed because Node picks the condition directly. + +## Key points + +* Node picks the **first-level key** (`import` or `require`) to resolve the + module. +* The `"types"` field is metadata for TypeScript, ignored by Node. +* This flattened structure removes the inner `default`/`types` objects while + keeping Node resolution and TypeScript typing intact. + +**Visual hierarchy:** + +``` +"." (package root) + ├─ "import" → JS entry for ESM + ├─ "require" → JS entry for CJS + └─ "types" → TS declaration (ignored by Node) +``` + +Node resolves the module using the **appropriate condition key**, so a nested +`"default"` object is unnecessary. diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/dist/index.js b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/dist/index.js new file mode 100644 index 00000000000..0a808c8f13e --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/dist/index.js @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ +export const hello = 'export'; diff --git a/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/package.json b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/package.json new file mode 100644 index 00000000000..ef0d99aa1e5 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/esm-exports/node_modules/string/package.json @@ -0,0 +1,6 @@ +{ + "name": "string", + "version": "1.0.0", + "description": "@see graal-nodejs/test/fixtures/self_ref_module/package.json", + "exports": "./dist/index.js" +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/NpmCompatibleESModuleLoader.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/NpmCompatibleESModuleLoader.java index 72011833b81..01164baf0c1 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/NpmCompatibleESModuleLoader.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/commonjs/NpmCompatibleESModuleLoader.java @@ -61,6 +61,8 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -99,9 +101,26 @@ public final class NpmCompatibleESModuleLoader extends DefaultESModuleLoader { private static final String INVALID_MODULE_SPECIFIER = "Invalid module specifier: '"; private static final String UNSUPPORTED_FILE_EXTENSION = "Unsupported file extension: '"; private static final String UNSUPPORTED_PACKAGE_EXPORTS = "Unsupported package exports: '"; + private static final String INVALID_PACKAGE_EXPORT = "Invalid package export: "; private static final String UNSUPPORTED_PACKAGE_IMPORTS = "Unsupported package imports: '"; private static final String UNSUPPORTED_DIRECTORY_IMPORT = "Unsupported directory import: '"; private static final String INVALID_PACKAGE_CONFIGURATION = "Invalid package configuration: '"; + private static final String EXPORT_TYPE_GRAALJS = "graaljs"; + private static final String EXPORT_TYPE_IMPORT = "import"; + private static final String EXPORT_TYPE_REQUIRE = "require"; + private static final String EXPORT_TYPE_DEFAULT = "default"; + private final LinkedList EXPORT_TYPES = new LinkedList<>( + List.of(EXPORT_TYPE_GRAALJS, EXPORT_TYPE_IMPORT, EXPORT_TYPE_REQUIRE, EXPORT_TYPE_DEFAULT)); + + public void registerPreferredExportType(String exportType) { + if (!EXPORT_TYPES.contains(exportType)) { + EXPORT_TYPES.addFirst(exportType); + } + } + + public List getRegisteredExportTypes() { + return List.copyOf(EXPORT_TYPES); + } public static NpmCompatibleESModuleLoader create(JSRealm realm) { return new NpmCompatibleESModuleLoader(realm); @@ -343,6 +362,10 @@ private Format esmFileFormat(URI url, TruffleLanguage.Env env) { if (url.getPath().endsWith(JS_EXT)) { return Format.ESM; } + } else if (url.getPath().endsWith(JS_EXT)) { + // Np Fallback to CJS as below (in the case that there is a package.json without a "type" field, or + // the "type" field is not "module"). + return Format.CommonJS; } } else if (url.getPath().endsWith(JS_EXT)) { // Np Package.json with .js extension: try loading as CJS like Node.js does. @@ -352,6 +375,27 @@ private Format esmFileFormat(URI url, TruffleLanguage.Env env) { throw fail(UNSUPPORTED_FILE_EXTENSION, url.toString()); } + private URI exportForImport(URI packageUrl, Map exports, TruffleLanguage.Env env) { + // in order of preference, find the best import to use for this circumstance; this will be `graaljs` if + // specified (as top preference), then `import`, then `require`, then `default`. if the developer has registered + // their own preferred export types, these will be preferred first. + // + // this branch only activates if package exports are present and need to be used to resolve an import. thus, + // there is no fallback behavior waiting for us, and so an exception is thrown if no export can be matched. + + // 1. for preferred export types... + for (String preferred : getRegisteredExportTypes()) { + // 1.1: is it specified within the exports? + if (exports.containsKey(preferred)) { + // 1.2: if so, resolve the import from the package root. make sure to slice off the `./` prefix. + return packageUrl.resolve(exports.get(preferred).substring(2)); + } + } + + // 2. if no preferred export types are specified, or none of them are found, throw an exception. + throw failMessage(UNSUPPORTED_PACKAGE_EXPORTS); + } + /** * PACKAGE_RESOLVE(packageSpecifier, parentURL). */ @@ -427,6 +471,12 @@ private URI packageResolve(String packageSpecifier, URI parentURL, TruffleLangua PackageJson pjson = readPackageJson(packageUrl, env); // 11.5 If pjson is not null and pjson.exports is not null or undefined, then if (pjson != null && pjson.hasExportsProperty()) { + var exp = pjson.getExport(packageSubpath); + if (exp != null) { + // we should receive a map of the form `type => path` for the requested export. determine the best + // import type to use and resolve from there. + return exportForImport(packageUrl, exp, env); + } throw fail(UNSUPPORTED_PACKAGE_EXPORTS, packageSpecifier); } else if (packageSubpath.equals(DOT)) { // 11.6 Otherwise, if packageSubpath is equal to ".", then @@ -547,6 +597,54 @@ public boolean hasExportsProperty() { return hasNonNullProperty(jsonObj, EXPORTS_PROPERTY_NAME); } + public Map getExport(String specifier) { + assert hasNonNullProperty(jsonObj, EXPORTS_PROPERTY_NAME); + var data = JSObject.get(jsonObj, EXPORTS_PROPERTY_NAME); + if (data instanceof JSDynamicObject exportsObj) { + for (TruffleString key : JSObject.enumerableOwnNames(exportsObj)) { + // find a match for the requested export... + if (key.toString().equals(specifier)) { + // if we found it, it should be a nested object with export mappings. at this point, we've + // already matched the path, so these are mappings of (type => path). `path` must be relative to + // the package root, must start with `.`, must not contain relative backwards references, and + // must be an extant regular file. + Object value = JSObject.get(exportsObj, key); + if (value instanceof JSDynamicObject valueObj) { + var exportKeys = valueObj.ownPropertyKeys(); + var exportMap = new HashMap(); + for (Object exportKey : exportKeys) { + if (exportKey instanceof TruffleString exportKeyStr) { + Object exportValue = JSObject.get(valueObj, exportKeyStr); + if (Strings.isTString(exportValue)) { + var exportStr = exportKeyStr.toString(); + var exportVal = exportValue.toString(); + if (!exportVal.startsWith(".") || exportVal.contains("..")) { + // must start with `.`, must not contain `..` + throw failMessage(INVALID_PACKAGE_EXPORT + exportStr); + } + exportMap.put(exportKeyStr.toString(), exportValue.toString()); + } + } else { + throw failMessage(UNSUPPORTED_PACKAGE_EXPORTS + exportKey.toString()); + } + } + return exportMap; + } else if (value instanceof TruffleString exportStr) { + // if the export is a string, it should be a path to the file to import. + if (!exportStr.toString().startsWith(".") || exportStr.toString().contains("..")) { + // must start with `.`, must not contain `..` + throw failMessage(INVALID_PACKAGE_EXPORT + exportStr); + } + return Map.of(EXPORT_TYPE_DEFAULT, exportStr.toString()); + } else { + throw failMessage(INVALID_PACKAGE_EXPORT + value); + } + } + } + } + return null; + } + public boolean hasMainProperty() { if (JSObject.hasProperty(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME)) { Object value = JSObject.get(jsonObj, PACKAGE_JSON_MAIN_PROPERTY_NAME);