[gjs/esm/static-imports: 6/9] esm: Enable static module imports.
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/esm/static-imports: 6/9] esm: Enable static module imports.
- Date: Thu, 4 Feb 2021 23:35:39 +0000 (UTC)
commit b5539f3a1796660e2cd0f83c5c79cf363bb077ff
Author: Evan Welsh <contact evanwelsh com>
Date: Thu Dec 3 19:35:59 2020 -0600
esm: Enable static module imports.
(Changes from Philip folded in: Jasmine tests, moving file operations into
internal.cpp)
doc/ESModules.md | 225 ++++++++++++
doc/Modules.md | 30 +-
gjs/context-private.h | 4 +
gjs/context.cpp | 185 ++++++++--
gjs/global.cpp | 84 +++++
gjs/global.h | 15 +
gjs/internal.cpp | 561 +++++++++++++++++++++++++++++
gjs/internal.h | 67 ++++
gjs/jsapi-util.cpp | 18 +
gjs/jsapi-util.h | 2 +
gjs/module.cpp | 130 +++++++
gjs/module.h | 15 +
installed-tests/js/.eslintrc.yml | 6 +
installed-tests/js/jsunit.gresources.xml | 1 +
installed-tests/js/meson.build | 19 +
installed-tests/js/modules/exports.js | 6 +
installed-tests/js/testESModules.js | 30 ++
installed-tests/minijasmine-module.test.in | 7 +
js.gresource.xml | 8 +
meson.build | 3 +-
modules/esm/.eslintrc.yml | 7 +
modules/esm/gi.js | 20 +
modules/internal/.eslintrc.yml | 29 ++
modules/internal/bootstrap/module.js | 209 +++++++++++
modules/internal/entry.js | 7 +
modules/internal/modules/esm.js | 203 +++++++++++
modules/internal/modules/gi.js | 22 ++
27 files changed, 1870 insertions(+), 43 deletions(-)
---
diff --git a/doc/ESModules.md b/doc/ESModules.md
new file mode 100644
index 00000000..a2566e67
--- /dev/null
+++ b/doc/ESModules.md
@@ -0,0 +1,225 @@
+# Modules: ECMAScript modules
+
+> _This documentation is inspired by [Node.js'
documentation](https://github.com/nodejs/node/blob/master/doc/api/esm.md)
+> on ECMAScript modules._
+
+ECMAScript Modules or "ES modules" are the [official ECMAScript
+standard][] for importing, exporting, and reusing JavaScript code.
+
+ES modules can export `function`, `class`, `const`, `let`, and `var`
+statements using the `export` keyword.
+
+```js
+// animalSounds.js
+export function bark(num) {
+ log('bark');
+}
+
+export const ANIMALS = ['dog', 'cat'];
+```
+
+Other ES modules can then import those declarations using `import`
+statements like the one below.
+
+```js
+// main.js
+import { ANIMALS, bark } from './animalSounds.js';
+
+// Logs 'bark'
+bark();
+
+// Logs 'dog, cat'
+log(ANIMALS);
+```
+
+## Loading ES Modules
+
+### Command Line
+
+From the command line ES modules can be loaded with the `-m` flag:
+
+```sh
+gjs -m module.js
+```
+
+### JavaScript
+
+ES modules cannot be loaded from strings at this time.
+
+`import('./module.js')` can be used to load modules from any GJS script
+or module.
+`import` will always default to loading a file as an ES Module.
+
+### C API
+
+Using the C API in `gjs.h`, ES modules can be loaded from a file or
+resource using `gjs_load_module_file()`. <!-- TODO -->
+
+## `import` Specifiers
+
+### Terminology
+
+The _specifier_ of an `import` statement is the string after the `from`
+keyword, e.g. `'path'` in `import { sep } from 'path'`.
+Specifiers are also used in `export from` statements, and as the
+argument to an `import()` expression.
+
+There are three types of specifiers:
+
+* _Relative specifiers_ like `'./window.js'`.
+ They refer to a path relative to the location of the importing file.
+ _The file extension is always necessary for these._
+
+* _Bare specifiers_ like `'some-package'`.
+ In GJS bare specifiers typically refer to built-in modules like `gi`.
+
+* _Absolute specifiers_ like `'file:///usr/share/gjs-app/file.js'`.
+ They refer directly and explicitly to a full path or library.
+
+Bare specifier resolutions import built-in modules.
+All other specifier resolutions are always only resolved with the
+standard relative URL resolution semantics.
+
+### Mandatory file extensions
+
+A file extension must be provided when using the `import` keyword to
+resolve relative or absolute specifiers.
+Directory files (e.g. `'./extensions/__init__.js'`) must also be fully
+specified.
+
+The recommended replacement for directory files (`__init__.js`) is:
+
+```js
+'./extensions.js'
+'./extensions/a.js'
+'./extensions/b.js'
+```
+
+Because file extensions are required, folders and `.js` files with the
+same "name" should not conflict as they did with `imports`.
+
+### URLs
+
+ES modules are resolved and cached as URLs.
+This means that files containing special characters such as `#` and `?`
+need to be escaped.
+
+`file:`, `resource:`, and `gi:` URL schemes are supported.
+A specifier like `'https://example.com/app.js'` is not supported in GJS.
+
+#### `file:` URLs
+
+Modules are loaded multiple times if the `import` specifier used to
+resolve them has a different query or fragment.
+
+```js
+import './foo.js?query=1'; // loads ./foo.js with query of "?query=1"
+import './foo.js?query=2'; // loads ./foo.js with query of "?query=2"
+```
+
+The root directory may be referenced via `file:///`.
+
+#### `gi:` Imports
+
+`gi:` URLs are supported as an alternative means to load GI (GObject
+Introspected) modules.
+
+`gi:` URLs support declaring libraries' versions.
+An error will be thrown when resolving imports if multiple versions of a
+library are present and a version has not been specified.
+The version is cached, so it only needs to be specified once.
+
+```js
+import Gtk from 'gi://Gtk?version=4.0';
+import Gdk from 'gi://Gdk?version=4.0';
+import GLib from 'gi://GLib';
+// GLib, GObject, and Gio are required by GJS so no version is necessary.
+```
+
+It is recommended to create a "version block" at your application's
+entry point.
+
+```js
+import 'gi://Gtk?version=3.0'
+import 'gi://Gdk?version=3.0'
+import 'gi://Hdy?version=1.0'
+```
+
+After these declarations, you can import the libraries without version
+parameters.
+
+```js
+import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
+import Hdy from 'gi://Hdy';
+```
+
+## `import()` expressions
+
+Dynamic [`import()` statements][] are not currently supported in GJS.
+
+## `import.meta`
+
+* {Object}
+
+The `import.meta` meta property is an `Object` that contains the
+following properties:
+
+### `import.meta.url`
+
+* {string} The absolute `file:` or `resource:` URL of the module.
+
+This is identical to Node.js and browser environments.
+It will always provide the URI of the current module.
+
+This enables useful patterns such as relative file loading:
+
+```js
+import Gio from 'gi://Gio';
+const file = Gio.File.new_for_uri(import.meta.url);
+const data = file.get_parent().resolve_relative_path('data.json');
+const contents = data.load_contents(null);
+```
+
+## Interoperability with legacy `imports` modules
+
+Because `imports` is a global object, it is still available in ES
+modules.
+It is not recommended to purposely mix import styles unless absolutely
+necessary.
+
+### `import` statements
+
+An `import` statement can only reference an ES module.
+`import` statements are permitted only in ES modules, but dynamic
+[`import()`][] expressions will be supported in legacy `imports` modules
+for loading ES modules.
+
+When importing legacy `imports` modules, all `var` declarations are
+provided as properties on the default export.
+
+### Differences between ES modules and legacy `imports` modules
+
+#### No `imports` and `var` exports
+
+You must use the [`export`][] syntax instead.
+
+#### No meta path properties
+
+These `imports` properties are not available in ES modules:
+
+ * `__modulePath__`
+ * `__moduleName__`
+ * `__parentModule__`
+
+`__modulePath__`, `__moduleName__` and `__parentModule__` use cases can
+be replaced with [`import.meta.url`][].
+
+[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
+[`import()`]: #esm_import_expressions
+[`import()` statements]:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports
+[`import.meta.url`]: #esm_import_meta_url
+[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
+[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
+[special scheme]: https://url.spec.whatwg.org/#special-scheme
+[official ECMAScript standard]: https://tc39.github.io/ecma262/#sec-modules
diff --git a/doc/Modules.md b/doc/Modules.md
index c3b9baf4..4c077bf5 100644
--- a/doc/Modules.md
+++ b/doc/Modules.md
@@ -2,7 +2,7 @@ GJS includes some built-in modules, as well as helpers for some core APIs like D
## [Gio](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gio.js)
-**Import with `const Gio = imports.gi.Gio;`**
+**Import with `const Gio = gi.require('Gio');` or `import Gio from 'gi://Gio'`**
The `Gio` override includes a number of utilities for DBus that will be documented further at a later date.
Below is a reasonable overview.
@@ -32,7 +32,7 @@ The `Gio` override includes a number of utilities for DBus that will be document
## [GLib](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GLib.js)
-**Import with `const GLib = imports.gi.GLib;`**
+**Import with `const GLib = gi.require('GLib');` or `import GLib from 'gi://GLib'`**
Mostly GVariant and GBytes compatibility.
@@ -43,13 +43,13 @@ Mostly GVariant and GBytes compatibility.
## [GObject](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GObject.js)
-**Import with `const GObject = imports.gi.GObject;`**
+**Import with `const GObject = gi.require('GObject');` or `import GObject from 'gi://GObject'`**
Mostly GObject implementation (properties, signals, GType mapping). May be useful as a reference.
## [Gtk](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gtk.js)
-**Import with `const Gtk = imports.gi.Gtk;`**
+**Import with `const Gtk = gi.require('Gtk', '3.0');` or `import Gtk from 'gi://Gtk'`**
Mostly GtkBuilder/composite template implementation. May be useful as a reference.
@@ -57,8 +57,7 @@ Mostly GtkBuilder/composite template implementation. May be useful as a referenc
**REMINDER:** You should specify a version prior to importing a library with multiple versions:
```js
-imports.gi.versions.Gtk = "3.0";
-const Gtk = imports.gi.Gtk;
+import Gtk from 'gi://Gtk?version=3.0';
```
@@ -284,10 +283,21 @@ Built-in version of the well-known [Tweener][tweener-www] animation/property tra
## GObject Introspection
+**Import with `import gi from 'gi';`**
+
+A wrapper of **libgirepository** to import native gobject-introspection libraries.
+
+* `gi.require(library: string, version?: string)`
+
+Loads a native gobject-introspection library. Version is required if more than one version of a library is
installed.
+You can also import libraries through the `gi://` URL scheme.
+
+### Legacy Imports (`imports.gi`)
+
**Import with `const gi = imports.gi;`**
-A wrapper of **libgirepository** to import native gobject-introspection libraries. This
-object has a property `versions` which is an object on which you can set string-valued
+A wrapper for **libgirepository** is also available via the global `imports` object.
+This object has a property `versions` which is an object on which you can set string-valued
properties indicating the version of that gobject-introspection library you want to load,
and loading multiple versions in the same process is forbidden. So if you want to
use gtk-3.0, set `imports.gi.versions.Gtk = '3.0';`.
@@ -295,7 +305,9 @@ use gtk-3.0, set `imports.gi.versions.Gtk = '3.0';`.
Any other properties of `imports.gi` will attempt to import a gobject-introspection library
with the property name, picking the latest version if there is no entry for it in `imports.gi.versions`.
-## More about **imports**
+## Legacy Imports
+
+Prior to the introduction of [ES Modules](ESModules.md), GJS had its own import system.
**imports** is a global object that you can use to import any js file or GObject
Introspection lib as module, there are 4 special properties of **imports**:
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 600ba05a..3a1db048 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -71,6 +71,7 @@ class GjsContextPrivate : public JS::JobQueue {
GjsContext* m_public_context;
JSContext* m_cx;
JS::Heap<JSObject*> m_global;
+ JS::Heap<JSObject*> m_internal_global;
GThread* m_owner_thread;
char* m_program_name;
@@ -170,6 +171,9 @@ class GjsContextPrivate : public JS::JobQueue {
}
[[nodiscard]] JSContext* context() const { return m_cx; }
[[nodiscard]] JSObject* global() const { return m_global.get(); }
+ [[nodiscard]] JSObject* internal_global() const {
+ return m_internal_global.get();
+ }
[[nodiscard]] GjsProfiler* profiler() const { return m_profiler; }
[[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; }
[[nodiscard]] bool destroying() const { return m_destroying; }
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 4f595048..1ec0b97e 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -33,11 +33,16 @@
#include <js/AllocPolicy.h> // for SystemAllocPolicy
#include <js/CallArgs.h> // for UndefinedHandleValue
+#include <js/CharacterEncoding.h>
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
+#include <js/ErrorReport.h>
+#include <js/Exception.h> // for StealPendingExceptionStack
#include <js/GCAPI.h> // for JS_GC, JS_AddExtraGCRootsTr...
#include <js/GCHashTable.h> // for WeakCache
#include <js/GCVector.h> // for RootedVector
+#include <js/Id.h>
+#include <js/Modules.h>
#include <js/Promise.h> // for JobQueue::SavedJobQueue
#include <js/PropertyDescriptor.h> // for JSPROP_PERMANENT, JSPROP_RE...
#include <js/RootingAPI.h>
@@ -62,8 +67,10 @@
#include "gjs/error-types.h"
#include "gjs/global.h"
#include "gjs/importer.h"
+#include "gjs/internal.h"
#include "gjs/jsapi-util.h"
#include "gjs/mem.h"
+#include "gjs/module.h"
#include "gjs/native.h"
#include "gjs/profiler-private.h"
#include "gjs/profiler.h"
@@ -304,6 +311,8 @@ gjs_context_class_init(GjsContextClass *klass)
void GjsContextPrivate::trace(JSTracer* trc, void* data) {
auto* gjs = static_cast<GjsContextPrivate*>(data);
JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
+ JS::TraceEdge<JSObject*>(trc, &gjs->m_internal_global,
+ "GJS internal global object");
gjs->m_atoms->trace(trc);
gjs->m_job_queue.trace(trc);
gjs->m_object_init_list.trace(trc);
@@ -390,6 +399,7 @@ void GjsContextPrivate::dispose(void) {
gjs_debug(GJS_DEBUG_CONTEXT, "Ending trace on global object");
JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
m_global = nullptr;
+ m_internal_global = nullptr;
gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
delete m_fundamental_table;
@@ -475,17 +485,17 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
m_atoms = new GjsAtoms();
- JS::RootedObject global(
- m_cx, gjs_create_global_object(cx, GjsGlobalType::DEFAULT));
+ JS::RootedObject internal_global(
+ m_cx, gjs_create_global_object(cx, GjsGlobalType::INTERNAL));
- if (!global) {
+ if (!internal_global) {
gjs_log_exception(m_cx);
- g_error("Failed to initialize global object");
+ g_error("Failed to initialize internal global object");
}
- JSAutoRealm ar(m_cx, global);
+ JSAutoRealm ar(m_cx, internal_global);
- m_global = global;
+ m_internal_global = internal_global;
JS_AddExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
if (!m_atoms->init_atoms(m_cx)) {
@@ -493,26 +503,83 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
g_error("Failed to initialize global strings");
}
- std::vector<std::string> paths;
- if (m_search_path)
- paths = {m_search_path, m_search_path + g_strv_length(m_search_path)};
- JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
- if (!importer) {
- gjs_log_exception(cx);
- g_error("Failed to create root importer");
+ if (!gjs_define_global_properties(m_cx, internal_global,
+ GjsGlobalType::INTERNAL,
+ "GJS internal global", "nullptr")) {
+ gjs_log_exception(m_cx);
+ g_error("Failed to define properties on internal global object");
}
- g_assert(
- gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
- "Someone else already created root importer");
+ JS::RootedObject global(
+ m_cx,
+ gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
- gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
- JS::ObjectValue(*importer));
+ if (!global) {
+ gjs_log_exception(m_cx);
+ g_error("Failed to initialize global object");
+ }
+
+ m_global = global;
- if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
- "GJS", "default")) {
+ {
+ JSAutoRealm ar(cx, global);
+
+ std::vector<std::string> paths;
+ if (m_search_path)
+ paths = {m_search_path,
+ m_search_path + g_strv_length(m_search_path)};
+ JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
+ if (!importer) {
+ gjs_log_exception(cx);
+ g_error("Failed to create root importer");
+ }
+
+ g_assert(
+ gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
+ "Someone else already created root importer");
+
+ gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
+ JS::ObjectValue(*importer));
+
+ if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
+ "GJS", "default")) {
+ gjs_log_exception(m_cx);
+ g_error("Failed to define properties on global object");
+ }
+ }
+
+ JS::SetModuleResolveHook(rt, gjs_module_resolve);
+ JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
+
+ if (!JS_DefineProperty(m_cx, internal_global, "moduleGlobalThis", global,
+ JSPROP_PERMANENT)) {
gjs_log_exception(m_cx);
- g_error("Failed to define properties on global object");
+ g_error("Failed to define module global in internal global.");
+ }
+
+ if (!gjs_load_internal_module(cx, "bootstrap/module")) {
+ gjs_log_exception(cx);
+ g_error("Failed to load internal module loaders.");
+ }
+
+ JS::RootedObject entry(
+ cx, gjs_module_load(
+ cx, "resource:///org/gnome/gjs/modules/internal/entry.js",
+ "resource:///org/gnome/gjs/modules/internal/entry.js"));
+
+ if (!entry) {
+ gjs_log_exception(cx);
+ g_error("Failed to load internal entry module.");
+ }
+
+ if (!JS::ModuleInstantiate(cx, entry)) {
+ gjs_log_exception(cx);
+ g_error("Failed to instantiate internal entry module.");
+ }
+
+ if (!JS::ModuleEvaluate(cx, entry)) {
+ gjs_log_exception(cx);
+ g_error("Failed to evaluate internal entry module.");
}
}
@@ -1033,25 +1100,77 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
bool GjsContextPrivate::eval_module(const char* identifier,
uint8_t* exit_status_p, GError** error) {
- *exit_status_p = 1;
- if (error)
- *error = nullptr;
+ AutoResetExit reset(this);
+
+ bool auto_profile = auto_profile_enter();
- g_error(
- "GjsContextPrivate::eval_module(%s) is not implemented. Exiting with "
- "error.",
- identifier);
+ JSAutoRealm ac(m_cx, m_global);
- return false;
+ JS::RootedObject registry(m_cx, gjs_get_module_registry(m_global));
+ JS::RootedId key(m_cx, gjs_intern_string_to_id(m_cx, identifier));
+ JS::RootedObject obj(m_cx);
+ if (!gjs_global_registry_get(m_cx, registry, key, &obj) || !obj) {
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Cannot find module with identifier: '%s'", identifier);
+ return false;
+ }
+
+ if (!JS::ModuleInstantiate(m_cx, obj)) {
+ gjs_log_exception(m_cx);
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Failed to resolve imports for module: '%s'", identifier);
+
+ return false;
+ }
+
+ bool ok = true;
+ if (!JS::ModuleEvaluate(m_cx, obj))
+ ok = false;
+
+ /* The promise job queue should be drained even on error, to finish
+ * outstanding async tasks before the context is torn down. Drain after
+ * uncaught exceptions have been reported since draining runs callbacks.
+ */
+ {
+ JS::AutoSaveExceptionState saved_exc(m_cx);
+ ok = run_jobs_fallible() && ok;
+ }
+
+ auto_profile_exit(auto_profile);
+
+ if (!ok) {
+ *exit_status_p = handle_exit_code("Module", identifier, error);
+ return false;
+ }
+
+ /* Assume success if no errors were thrown or exit code set. */
+ *exit_status_p = 0;
+ return true;
}
bool GjsContextPrivate::register_module(const char* identifier, const char* uri,
GError** error) {
- if (error)
- *error = nullptr;
+ JSAutoRealm ar(m_cx, m_global);
- g_warning("Identifier: %s\nURI: %s\n", identifier, uri);
- return true;
+ if (gjs_module_load(m_cx, identifier, uri))
+ return true;
+
+ const char* msg = "unknown";
+ JS::ExceptionStack exn_stack(m_cx);
+ JS::ErrorReportBuilder builder(m_cx);
+ if (JS::StealPendingExceptionStack(m_cx, &exn_stack) &&
+ builder.init(m_cx, exn_stack,
+ JS::ErrorReportBuilder::WithSideEffects)) {
+ msg = builder.toStringResult().c_str();
+ } else {
+ JS_ClearPendingException(m_cx);
+ }
+
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Failed to parse module '%s': %s", identifier,
+ msg ? msg : "unknown");
+
+ return false;
}
bool
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 3a1fc84b..212ad04b 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -31,6 +31,7 @@
#include "gjs/context-private.h"
#include "gjs/engine.h"
#include "gjs/global.h"
+#include "gjs/internal.h"
#include "gjs/jsapi-util.h"
#include "gjs/native.h"
@@ -187,6 +188,13 @@ class GjsGlobal : GjsBaseGlobal {
gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
JS::ObjectValue(*native_registry));
+ JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
+ if (!module_registry)
+ return false;
+
+ gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+ JS::ObjectValue(*module_registry));
+
JS::Value v_importer =
gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
g_assert(((void) "importer should be defined before passing null "
@@ -255,6 +263,70 @@ class GjsDebuggerGlobal : GjsBaseGlobal {
}
};
+class GjsInternalGlobal : GjsBaseGlobal {
+ static constexpr JSFunctionSpec static_funcs[] = {
+ JS_FN("compileModule", gjs_internal_compile_module, 2, 0),
+ JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2,
+ 0),
+ JS_FN("getRegistry", gjs_internal_global_get_registry, 1, 0),
+ JS_FN("importSync", gjs_internal_global_import_sync, 1, 0),
+ JS_FN("loadResourceOrFile", gjs_internal_load_resource_or_file, 1, 0),
+ JS_FN("parseURI", gjs_internal_parse_uri, 1, 0),
+ JS_FN("resolveRelativeResourceOrFile",
+ gjs_internal_resolve_relative_resource_or_file, 2, 0),
+ JS_FN("setModuleLoadHook", gjs_internal_global_set_module_hook, 3, 0),
+ JS_FN("setModuleMetaHook", gjs_internal_global_set_module_meta_hook, 2,
+ 0),
+ JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
+ JS_FN("setModuleResolveHook",
+ gjs_internal_global_set_module_resolve_hook, 2, 0),
+ JS_FN("uriExists", gjs_internal_uri_exists, 1, 0),
+ JS_FS_END};
+
+ static constexpr JSClass klass = {
+ "GjsInternalGlobal",
+ JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+ static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+ &defaultclassops,
+ };
+
+ public:
+ [[nodiscard]] static JSObject* create(JSContext* cx) {
+ return GjsBaseGlobal::create(cx, &klass);
+ }
+
+ [[nodiscard]] static JSObject* create_with_compartment(
+ JSContext* cx, JS::HandleObject cmp_global) {
+ return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass);
+ }
+
+ static bool define_properties(JSContext* cx, JS::HandleObject global,
+ const char* realm_name,
+ const char* bootstrap_script G_GNUC_UNUSED) {
+ JS::Realm* realm = JS::GetObjectRealmOrNull(global);
+ g_assert(realm && "Global object must be associated with a realm");
+ // const_cast is allowed here if we never free the realm data
+ JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+
+ JSAutoRealm ar(cx, global);
+ JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
+ if (!native_registry)
+ return false;
+
+ gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
+ JS::ObjectValue(*native_registry));
+
+ JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
+ if (!module_registry)
+ return false;
+
+ gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+ JS::ObjectValue(*module_registry));
+
+ return JS_DefineFunctions(cx, global, static_funcs);
+ }
+};
+
/**
* gjs_create_global_object:
* @cx: a #JSContext
@@ -273,6 +345,9 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
case GjsGlobalType::DEBUGGER:
return GjsDebuggerGlobal::create_with_compartment(
cx, current_global);
+ case GjsGlobalType::INTERNAL:
+ return GjsInternalGlobal::create_with_compartment(
+ cx, current_global);
default:
return nullptr;
}
@@ -283,6 +358,8 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
return GjsGlobal::create(cx);
case GjsGlobalType::DEBUGGER:
return GjsDebuggerGlobal::create(cx);
+ case GjsGlobalType::INTERNAL:
+ return GjsInternalGlobal::create(cx);
default:
return nullptr;
}
@@ -435,6 +512,9 @@ bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global,
case GjsGlobalType::DEBUGGER:
return GjsDebuggerGlobal::define_properties(cx, global, realm_name,
bootstrap_script);
+ case GjsGlobalType::INTERNAL:
+ return GjsInternalGlobal::define_properties(cx, global, realm_name,
+ bootstrap_script);
}
// Global type does not handle define_properties
@@ -456,3 +536,7 @@ decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props;
decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass;
decltype(
GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs;
+
+decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass;
+decltype(
+ GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index 2b7c0e16..f39c4c8c 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -23,6 +23,7 @@ struct PropertyKey;
enum class GjsGlobalType {
DEFAULT,
DEBUGGER,
+ INTERNAL,
};
enum class GjsBaseGlobalSlot : uint32_t {
@@ -36,6 +37,14 @@ enum class GjsDebuggerGlobalSlot : uint32_t {
enum class GjsGlobalSlot : uint32_t {
IMPORTS = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+ // Stores a function which resolves imports
+ IMPORT_HOOK,
+ // Stores a function which creates new module objects
+ MODULE_HOOK,
+ // Stores a function which sets the metadata on import.meta
+ META_HOOK,
+ // Stores the module registry (a Map object)
+ MODULE_REGISTRY,
NATIVE_REGISTRY,
PROTOTYPE_gtype,
PROTOTYPE_importer,
@@ -58,6 +67,10 @@ enum class GjsGlobalSlot : uint32_t {
LAST,
};
+enum class GjsInternalGlobalSlot : uint32_t {
+ LAST = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+};
+
bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
GjsGlobalType gjs_global_get_type(JSContext* cx);
GjsGlobalType gjs_global_get_type(JSObject* global);
@@ -89,6 +102,7 @@ template <typename Slot>
inline void gjs_set_global_slot(JSObject* global, Slot slot, JS::Value value) {
static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
std::is_same_v<GjsGlobalSlot, Slot> ||
+ std::is_same_v<GjsInternalGlobalSlot, Slot> ||
std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
"Must use a GJS global slot enum");
detail::set_global_slot(global, static_cast<uint32_t>(slot), value);
@@ -98,6 +112,7 @@ template <typename Slot>
inline JS::Value gjs_get_global_slot(JSObject* global, Slot slot) {
static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
std::is_same_v<GjsGlobalSlot, Slot> ||
+ std::is_same_v<GjsInternalGlobalSlot, Slot> ||
std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
"Must use a GJS global slot enum");
return detail::get_global_slot(global, static_cast<uint32_t>(slot));
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
new file mode 100644
index 00000000..08669943
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,561 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+#include "gjs/internal.h"
+
+#include <config.h>
+#include <gio/gio.h>
+#include <girepository.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <js/Array.h>
+#include <js/Class.h>
+#include <js/CompilationAndEvaluation.h>
+#include <js/CompileOptions.h>
+#include <js/Conversions.h>
+#include <js/GCVector.h> // for RootedVector
+#include <js/Modules.h>
+#include <js/Promise.h>
+#include <js/PropertyDescriptor.h>
+#include <js/RootingAPI.h>
+#include <js/SourceText.h>
+#include <js/TypeDecls.h>
+#include <js/Wrapper.h>
+#include <jsapi.h> // for JS_DefinePropertyById, ...
+#include <jsfriendapi.h>
+#include <stddef.h> // for size_t
+#include <sys/types.h> // for ssize_t
+
+#include <codecvt> // for codecvt_utf8_utf16
+#include <locale> // for wstring_convert
+#include <string> // for u16string
+#include <vector>
+
+#include "gjs/byteArray.h"
+#include "gjs/context-private.h"
+#include "gjs/context.h"
+#include "gjs/engine.h"
+#include "gjs/error-types.h"
+#include "gjs/global.h"
+#include "gjs/importer.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/mem-private.h"
+#include "gjs/module.h"
+#include "gjs/native.h"
+#include "util/log.h"
+
+#include "gi/repo.h"
+
+// NOTE: You have to be very careful in this file to only do operations within
+// the correct global!
+
+/**
+ * gjs_load_internal_module:
+ *
+ * @brief Loads a module source from an internal resource,
+ * resource:///org/gnome/gjs/modules/internal/{#identifier}.js, registers it in
+ * the internal global's module registry, and proceeds to compile, initialize,
+ * and evaluate the module.
+ *
+ * @param cx the current JSContext
+ * @param identifier the identifier of the internal module
+ *
+ * @returns whether an error occurred while loading or evaluating the module.
+ */
+bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
+ GjsAutoChar full_path(g_strdup_printf(
+ "resource:///org/gnome/gjs/modules/internal/%s.js", identifier));
+
+ char* script;
+ size_t script_len;
+
+ if (!gjs_load_internal_source(cx, full_path, &script, &script_len))
+ return false;
+
+ std::u16string utf16_string = gjs_utf8_script_to_utf16(script, script_len);
+ g_free(script);
+
+ // COMPAT: This could use JS::SourceText<mozilla::Utf8Unit> directly,
+ // but that messes up code coverage. See bug
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784
+ JS::SourceText<char16_t> buf;
+ if (!buf.init(cx, utf16_string.c_str(), utf16_string.size(),
+ JS::SourceOwnership::Borrowed))
+ return false;
+
+ JS::CompileOptions options(cx);
+ options.setIntroductionType("Internal Module Bootstrap");
+ options.setFileAndLine(full_path, 1);
+ options.setSelfHostingMode(false);
+
+ JS::RootedObject internal_global(cx, gjs_get_internal_global(cx));
+ JSAutoRealm ar(cx, internal_global);
+
+ JS::RootedObject module(cx, JS::CompileModule(cx, options, buf));
+ JS::RootedObject registry(cx, gjs_get_module_registry(internal_global));
+
+ JS::RootedId key(cx, gjs_intern_string_to_id(cx, full_path));
+
+ if (!gjs_global_registry_set(cx, registry, key, module) ||
+ !JS::ModuleInstantiate(cx, module) || !JS::ModuleEvaluate(cx, module)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Asserts the correct arguments for a hook setting function.
+ *
+ * Asserts: (arg0: object, arg1: Function) => void
+ */
+static void set_module_hook(JS::CallArgs args, GjsGlobalSlot slot) {
+ JS::Value v_global = args[0];
+ JS::Value v_hook = args[1];
+
+ g_assert(v_global.isObject());
+ g_assert(v_hook.isObject());
+
+ g_assert(JS::IsCallable(&v_hook.toObject()));
+ gjs_set_global_slot(&v_global.toObject(), slot, v_hook);
+
+ args.rval().setUndefined();
+}
+
+/**
+ * gjs_internal_global_set_module_hook:
+ *
+ * @brief Sets the MODULE_HOOK slot of the passed global object.
+ * Asserts that the second argument must be callable (e.g. Function)
+ * The passed callable is called by gjs_module_load.
+ *
+ * @example (in JavaScript)
+ * setModuleLoadHook(globalThis, (id, uri) => {
+ * id // the module's identifier
+ * uri // the URI to load from
+ * });
+ *
+ * @returns guaranteed to return true or assert.
+ */
+bool gjs_internal_global_set_module_hook([[maybe_unused]] JSContext* cx,
+ unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "setModuleLoadHook takes 2 arguments");
+
+ set_module_hook(args, GjsGlobalSlot::MODULE_HOOK);
+ return true;
+}
+
+/**
+ * gjs_internal_global_set_module_resolve_hook:
+ *
+ * @brief Sets the IMPORT_HOOK slot of the passed global object.
+ * Asserts that the second argument must be callable (e.g. Function)
+ * The passed callable is called by gjs_module_resolve.
+ *
+ * @example (in JavaScript)
+ * setModuleResolveHook(globalThis, (module, specifier) => {
+ * module // the importing module object
+ * specifier // the import specifier
+ * });
+ *
+ * @returns guaranteed to return true or assert.
+ */
+bool gjs_internal_global_set_module_resolve_hook([[maybe_unused]] JSContext* cx,
+ unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "setModuleResolveHook takes 2 arguments");
+
+ set_module_hook(args, GjsGlobalSlot::IMPORT_HOOK);
+ return true;
+}
+
+/**
+ * gjs_internal_global_set_module_meta_hook:
+ *
+ * @brief Sets the META_HOOK slot of the passed passed global object.
+ * Asserts that the second argument must be callable (e.g. Function).
+ * The passed callable is called by gjs_populate_module_meta.
+ *
+ * The META_HOOK is passed two parameters, a plain object for population with
+ * meta properties and the module's private object.
+ *
+ * @example (in JavaScript)
+ * setModuleMetaHook(globalThis, (module, meta) => {
+ * module // the module object
+ * meta // the meta object
+ * });
+ *
+ * @returns guaranteed to return true or assert.
+ */
+bool gjs_internal_global_set_module_meta_hook([[maybe_unused]] JSContext* cx,
+ unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "setModuleMetaHook takes 2 arguments");
+
+ set_module_hook(args, GjsGlobalSlot::META_HOOK);
+ return true;
+}
+
+/**
+ * compile_module:
+ *
+ * @brief Compiles the a module source text into an internal #Module object
+ * given the module's URI as the first argument.
+ *
+ * @param cx the current JSContext
+ * @param args the call args from the native function call
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+static bool compile_module(JSContext* cx, JS::CallArgs args) {
+ g_assert(args[0].isString());
+ g_assert(args[1].isString());
+
+ JS::RootedString s1(cx, args[0].toString());
+ JS::RootedString s2(cx, args[1].toString());
+
+ JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, s1);
+ if (!uri)
+ return false;
+
+ JS::CompileOptions options(cx);
+ options.setFileAndLine(uri.get(), 1).setSourceIsLazy(false);
+
+ size_t text_len;
+ char16_t* text;
+ if (!gjs_string_get_char16_data(cx, s2, &text, &text_len))
+ return false;
+
+ JS::SourceText<char16_t> buf;
+ if (!buf.init(cx, text, text_len, JS::SourceOwnership::TakeOwnership))
+ return false;
+
+ JS::RootedObject new_module(cx, JS::CompileModule(cx, options, buf));
+ if (!new_module)
+ return false;
+
+ args.rval().setObject(*new_module);
+ return true;
+}
+
+/**
+ * gjs_internal_compile_internal_module:
+ *
+ * @brief Compiles a module source text within the internal global's realm.
+ *
+ * NOTE: Modules compiled with this function can only be executed
+ * within the internal global's realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+bool gjs_internal_compile_internal_module(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "compileInternalModule takes 2 arguments");
+
+ JS::RootedObject global(cx, gjs_get_internal_global(cx));
+ JSAutoRealm ar(cx, global);
+ return compile_module(cx, args);
+}
+
+/**
+ * gjs_internal_compile_module:
+ *
+ * @brief Compiles a module source text within the import global's realm.
+ *
+ * NOTE: Modules compiled with this function can only be executed
+ * within the import global's realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+bool gjs_internal_compile_module(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "compileModule takes 2 arguments");
+
+ JS::RootedObject global(cx, gjs_get_import_global(cx));
+ JSAutoRealm ar(cx, global);
+ return compile_module(cx, args);
+}
+
+/**
+ * gjs_internal_set_module_private:
+ *
+ * @brief Sets the private object of an internal #Module object.
+ * The private object must be a #JSObject.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while setting the private.
+ */
+bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "setModulePrivate takes 2 arguments");
+ g_assert(args[0].isObject());
+ g_assert(args[1].isObject());
+
+ JS::RootedObject moduleObj(cx, &args[0].toObject());
+ JS::RootedObject privateObj(cx, &args[1].toObject());
+
+ JS::SetModulePrivate(moduleObj, JS::ObjectValue(*privateObj));
+ return true;
+}
+
+/**
+ * gjs_internal_global_import_sync:
+ *
+ * @brief Synchronously imports native "modules" from the import global's
+ * native registry. This function does not do blocking I/O so it is
+ * safe to call it synchronously for accessing native "modules" within
+ * modules. This function is always called within the import global's
+ * realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while importing the native module.
+ */
+bool gjs_internal_global_import_sync(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JS::UniqueChars id;
+ if (!gjs_parse_call_args(cx, "importSync", args, "s", "identifier", &id))
+ return false;
+
+ JS::RootedObject global(cx, gjs_get_import_global(cx));
+ JSAutoRealm ar(cx, global);
+
+ JS::AutoSaveExceptionState exc_state(cx);
+
+ JS::RootedObject native_registry(cx, gjs_get_native_registry(global));
+ JS::RootedObject v_module(cx);
+
+ JS::RootedId key(cx, gjs_intern_string_to_id(cx, id.get()));
+ if (!gjs_global_registry_get(cx, native_registry, key, &v_module))
+ return false;
+
+ if (v_module) {
+ args.rval().setObject(*v_module);
+ return true;
+ }
+
+ JS::RootedObject native_obj(cx);
+ if (!gjs_load_native_module(cx, id.get(), &native_obj)) {
+ gjs_throw(cx, "Failed to load native module: %s", id.get());
+ return false;
+ }
+
+ if (!gjs_global_registry_set(cx, native_registry, key, native_obj))
+ return false;
+
+ args.rval().setObject(*native_obj);
+ return true;
+}
+
+/**
+ * gjs_internal_global_get_registry:
+ *
+ * @brief Retrieves the module registry for the passed global object.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while retrieving the registry.
+ */
+bool gjs_internal_global_get_registry(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 1 && "getRegistry takes 1 arguments");
+ g_assert(args[0].isObject());
+
+ JS::RootedObject global(cx, &args[0].toObject());
+ JSAutoRealm ar(cx, global);
+
+ JS::RootedObject registry(cx, gjs_get_module_registry(global));
+ args.rval().setObject(*registry);
+ return true;
+}
+
+bool gjs_internal_parse_uri(JSContext* cx, unsigned argc, JS::Value* vp) {
+ using AutoHashTable =
+ GjsAutoPointer<GHashTable, GHashTable, g_hash_table_destroy>;
+ using AutoURI = GjsAutoPointer<GUri, GUri, g_uri_unref>;
+
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ g_assert(args.length() == 1 && "parseUri() takes one string argument");
+ g_assert(args[0].isString() && "parseUri() takes one string argument");
+
+ JS::RootedString string_arg(cx, args[0].toString());
+ JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+ if (!uri)
+ return false;
+
+ GError* error = nullptr;
+ AutoURI parsed = g_uri_parse(uri.get(), G_URI_FLAGS_NONE, &error);
+ if (!parsed)
+ return gjs_throw_gerror_message(cx, error);
+
+ JS::RootedObject query_obj(cx, JS_NewPlainObject(cx));
+ if (!query_obj)
+ return false;
+
+ const char* raw_query = g_uri_get_query(parsed);
+ if (raw_query) {
+ AutoHashTable query =
+ g_uri_parse_params(raw_query, -1, "&", G_URI_PARAMS_NONE, &error);
+ if (!query)
+ return gjs_throw_gerror_message(cx, error);
+
+ GHashTableIter iter;
+ g_hash_table_iter_init(&iter, query);
+
+ void* key_ptr;
+ void* value_ptr;
+ while (g_hash_table_iter_next(&iter, &key_ptr, &value_ptr)) {
+ auto* key = static_cast<const char*>(key_ptr);
+ auto* value = static_cast<const char*>(value_ptr);
+
+ JS::ConstUTF8CharsZ value_chars{value, strlen(value)};
+ JS::RootedString value_str(cx,
+ JS_NewStringCopyUTF8Z(cx, value_chars));
+ if (!value_str || !JS_DefineProperty(cx, query_obj, key, value_str,
+ JSPROP_ENUMERATE))
+ return false;
+ }
+ }
+
+ JS::RootedObject return_obj(cx, JS_NewPlainObject(cx));
+ if (!return_obj)
+ return false;
+
+ // JS_NewStringCopyZ() used here and below because the URI components are
+ // %-encoded, meaning ASCII-only
+ JS::RootedString scheme(cx,
+ JS_NewStringCopyZ(cx, g_uri_get_scheme(parsed)));
+ if (!scheme)
+ return false;
+
+ JS::RootedString host(cx, JS_NewStringCopyZ(cx, g_uri_get_host(parsed)));
+ if (!host)
+ return false;
+
+ JS::RootedString path(cx, JS_NewStringCopyZ(cx, g_uri_get_path(parsed)));
+ if (!path)
+ return false;
+
+ if (!JS_DefineProperty(cx, return_obj, "uri", string_arg,
+ JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, return_obj, "scheme", scheme,
+ JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, return_obj, "host", host, JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, return_obj, "path", path, JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, return_obj, "query", query_obj,
+ JSPROP_ENUMERATE))
+ return false;
+
+ args.rval().setObject(*return_obj);
+ return true;
+}
+
+bool gjs_internal_resolve_relative_resource_or_file(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ g_assert(args.length() == 2 && "resolveRelativeResourceOrFile(str, str)");
+ g_assert(args[0].isString() && "resolveRelativeResourceOrFile(str, str)");
+ g_assert(args[1].isString() && "resolveRelativeResourceOrFile(str, str)");
+
+ JS::RootedString string_arg(cx, args[0].toString());
+ JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+ if (!uri)
+ return false;
+ string_arg = args[1].toString();
+ JS::UniqueChars relative_path = JS_EncodeStringToUTF8(cx, string_arg);
+ if (!relative_path)
+ return false;
+
+ GjsAutoUnref<GFile> module_file = g_file_new_for_uri(uri.get());
+ GjsAutoUnref<GFile> module_parent_file = g_file_get_parent(module_file);
+
+ if (module_parent_file) {
+ GjsAutoUnref<GFile> output = g_file_resolve_relative_path(
+ module_parent_file, relative_path.get());
+ GjsAutoChar output_uri = g_file_get_uri(output);
+
+ JS::ConstUTF8CharsZ uri_chars(output_uri, strlen(output_uri));
+ JS::RootedString retval(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
+ if (!retval)
+ return false;
+
+ args.rval().setString(retval);
+ return true;
+ }
+
+ args.rval().setNull();
+ return true;
+}
+
+bool gjs_internal_load_resource_or_file(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ g_assert(args.length() == 1 && "loadResourceOrFile(str)");
+ g_assert(args[0].isString() && "loadResourceOrFile(str)");
+
+ JS::RootedString string_arg(cx, args[0].toString());
+ JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+ if (!uri)
+ return false;
+
+ GjsAutoUnref<GFile> file = g_file_new_for_uri(uri.get());
+
+ char* contents;
+ size_t length;
+ GError* error = nullptr;
+ if (!g_file_load_contents(file, /* cancellable = */ nullptr, &contents,
+ &length, /* etag_out = */ nullptr, &error))
+ return gjs_throw_gerror_message(cx, error);
+
+ JS::ConstUTF8CharsZ contents_chars{contents, length};
+ JS::RootedString contents_str(cx,
+ JS_NewStringCopyUTF8Z(cx, contents_chars));
+ g_free(contents);
+ if (!contents_str)
+ return false;
+
+ args.rval().setString(contents_str);
+ return true;
+}
+
+bool gjs_internal_uri_exists(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ g_assert(args.length() >= 1 && "uriExists(str)"); // extra args are OK
+ g_assert(args[0].isString() && "uriExists(str)");
+
+ JS::RootedString string_arg(cx, args[0].toString());
+ JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+ if (!uri)
+ return false;
+
+ GjsAutoUnref<GFile> file = g_file_new_for_uri(uri.get());
+
+ args.rval().setBoolean(g_file_query_exists(file, nullptr));
+ return true;
+}
diff --git a/gjs/internal.h b/gjs/internal.h
new file mode 100644
index 00000000..dd9b38e8
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+#ifndef GJS_INTERNAL_H_
+#define GJS_INTERNAL_H_
+
+#include <config.h>
+
+#include <js/TypeDecls.h>
+#include <jsapi.h>
+
+#include "gjs/macros.h"
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_load_internal_module(JSContext* cx, const char* identifier);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_compile_module(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_compile_internal_module(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_global_get_registry(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_global_import_sync(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_global_set_module_hook(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_global_set_module_meta_hook(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_global_set_module_resolve_hook(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_parse_uri(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_resolve_relative_resource_or_file(JSContext* cx,
+ unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_load_resource_or_file(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_load_resource_or_file_async(JSContext* cx, unsigned argc,
+ JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_uri_exists(JSContext* cx, unsigned argc, JS::Value* vp);
+
+#endif // GJS_INTERNAL_H_
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 1e7d2aa8..6262ac2d 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -621,6 +621,24 @@ JSObject* gjs_get_import_global(JSContext* cx) {
return GjsContextPrivate::from_cx(cx)->global();
}
+/**
+ * gjs_get_internal_global:
+ *
+ * @brief Gets the "internal global" for the context's runtime. The internal
+ * global object is the global object used for all "internal" JavaScript
+ * code (e.g. the module loader) that should not be accessible from users'
+ * code.
+ *
+ * @param cx a #JSContext
+ *
+ * @returns the "internal global" for the context's
+ * runtime. Will never return %NULL while GJS has an active context
+ * for the runtime.
+ */
+JSObject* gjs_get_internal_global(JSContext* cx) {
+ return GjsContextPrivate::from_cx(cx)->internal_global();
+}
+
#if defined(G_OS_WIN32) && (defined(_MSC_VER) && (_MSC_VER >= 1900))
/* Unfortunately Visual Studio's C++ .lib somehow did not contain the right
* codecvt stuff that we need to convert from utf8 to utf16 (char16_t), so we
diff --git a/gjs/jsapi-util.h b/gjs/jsapi-util.h
index 273e9a29..11c23776 100644
--- a/gjs/jsapi-util.h
+++ b/gjs/jsapi-util.h
@@ -387,6 +387,8 @@ struct GCPolicy<GjsAutoParam> : public IgnoreGCPolicy<GjsAutoParam> {};
[[nodiscard]] JSObject* gjs_get_import_global(JSContext* cx);
+[[nodiscard]] JSObject* gjs_get_internal_global(JSContext* cx);
+
void gjs_throw_constructor_error (JSContext *context);
void gjs_throw_abstract_constructor_error(JSContext* cx,
diff --git a/gjs/module.cpp b/gjs/module.cpp
index d10b4727..77108af7 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -5,6 +5,7 @@
#include <config.h>
#include <stddef.h> // for size_t
+#include <string.h>
#include <sys/types.h> // for ssize_t
#include <string> // for u16string
@@ -12,6 +13,7 @@
#include <gio/gio.h>
#include <glib.h>
+#include <js/CharacterEncoding.h> // for ConstUTF8CharsZ
#include <js/Class.h>
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
@@ -21,6 +23,7 @@
#include <js/SourceText.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
+#include <js/ValueArray.h>
#include <jsapi.h> // for JS_DefinePropertyById, ...
#include "gjs/context-private.h"
@@ -269,3 +272,130 @@ JSObject* gjs_get_native_registry(JSObject* global) {
g_assert(native_registry.isObject());
return &native_registry.toObject();
}
+
+/**
+ * gjs_get_module_registry:
+ *
+ * @brief Retrieves a global's module registry from the MODULE_REGISTRY slot.
+ * Registries are JS Maps. See gjs_get_native_registry for more detail.
+ *
+ * @param cx the current #JSContext
+ * @param global a global #JSObject
+ *
+ * @returns the registry map as a #JSObject
+ */
+JSObject* gjs_get_module_registry(JSObject* global) {
+ JS::Value esm_registry =
+ gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
+
+ g_assert(esm_registry.isObject());
+ return &esm_registry.toObject();
+}
+
+/**
+ * gjs_module_load:
+ *
+ * Loads and registers a module given a specifier and
+ * URI.
+ *
+ * @param importer the private value of the #Module object initiating the import
+ * or undefined.
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_load(JSContext* cx, const char* identifier,
+ const char* file_uri) {
+ g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
+ gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
+ "gjs_module_load can only be called from module-enabled "
+ "globals.");
+
+ JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+ JS::RootedValue hook(
+ cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_HOOK));
+
+ JS::ConstUTF8CharsZ id_chars(identifier, strlen(identifier));
+ JS::ConstUTF8CharsZ uri_chars(file_uri, strlen(file_uri));
+ JS::RootedString id(cx, JS_NewStringCopyUTF8Z(cx, id_chars));
+ if (!id)
+ return nullptr;
+ JS::RootedString uri(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
+ if (!uri)
+ return nullptr;
+
+ JS::RootedValueArray<2> args(cx);
+ args[0].setString(id);
+ args[1].setString(uri);
+
+ JS::RootedValue result(cx);
+ if (!JS_CallFunctionValue(cx, nullptr, hook, args, &result))
+ return nullptr;
+
+ g_assert(result.isObject() && "Module hook failed to return an object!");
+ return &result.toObject();
+}
+
+/**
+ * gjs_populate_module_meta:
+ *
+ * Hook SpiderMonkey calls to populate the import.meta object.
+ *
+ * @param private_ref the private value for the #Module object
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while populating the module meta.
+ */
+bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
+ JS::HandleObject meta_object_handle) {
+ g_assert(private_ref.isObject());
+
+ JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+ JS::RootedValue hook(cx,
+ gjs_get_global_slot(global, GjsGlobalSlot::META_HOOK));
+
+ JS::RootedObject meta(cx, meta_object_handle);
+ JS::RootedObject module(cx, &private_ref.toObject());
+ JS::RootedValueArray<2> args(cx);
+ args[0].setObject(*module);
+ args[1].setObject(*meta);
+
+ JS::RootedValue ignore_result(cx);
+ if (!JS_CallFunctionValue(cx, nullptr, hook, args, &ignore_result))
+ return false;
+
+ return true;
+}
+
+/**
+ * gjs_module_resolve:
+ *
+ * Hook SpiderMonkey calls to resolve import specifiers.
+ *
+ * @param importer the private value of the #Module object initiating the import
+ * or undefined.
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importer,
+ JS::HandleString specifier) {
+ g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
+ gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
+ "gjs_module_resolve can only be called from module-enabled "
+ "globals.");
+
+ JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+ JS::RootedValue v_hook(
+ cx, gjs_get_global_slot(global, GjsGlobalSlot::IMPORT_HOOK));
+
+ JS::RootedValueArray<2> args(cx);
+ args[0].set(importer);
+ args[1].setString(specifier);
+
+ JS::RootedValue result(cx);
+ if (!JS_CallFunctionValue(cx, nullptr, v_hook, args, &result))
+ return nullptr;
+
+ return &result.toObject();
+}
diff --git a/gjs/module.h b/gjs/module.h
index 0d35af26..c754b114 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -24,4 +24,19 @@ gjs_module_import(JSContext *cx,
GJS_JSAPI_RETURN_CONVENTION
JSObject* gjs_get_native_registry(JSObject* global);
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_get_module_registry(JSObject* global);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_load(JSContext* cx, const char* identifier,
+ const char* uri);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
+ JS::HandleString specifier);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
+ JS::HandleObject meta_object);
+
#endif // GJS_MODULE_H_
diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml
index a9c37e58..2ecbdb1c 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -29,3 +29,9 @@ globals:
clearTimeout: writable
setInterval: writable
setTimeout: writable
+overrides:
+ - files:
+ - testESModules.js
+ - modules/exports.js
+ parserOptions:
+ sourceType: module
diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml
index 674e342f..5503446d 100644
--- a/installed-tests/js/jsunit.gresources.xml
+++ b/installed-tests/js/jsunit.gresources.xml
@@ -12,6 +12,7 @@
<file>modules/badOverrides/Gio.js</file>
<file>modules/badOverrides/Regress.js</file>
<file>modules/badOverrides/WarnLib.js</file>
+ <file>modules/exports.js</file>
<file>modules/foobar.js</file>
<file>modules/lexicalScope.js</file>
<file>modules/modunicode.js</file>
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index dae48e07..58385030 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -194,3 +194,22 @@ gdbus_test_description = configure_file(
if get_option('installed_tests')
install_data('testGDBus.js', install_dir: installed_js_tests_dir)
endif
+
+# testESModules.js is also separate because it needs an extra minijasmine flag
+
+test('ESModules', minijasmine, args: [files('testESModules.js'), '-m'],
+ env: tests_environment, protocol: 'tap', suite: 'JS')
+
+esm_test_description_subst = {
+ 'name': 'testESModules.js',
+ 'installed_tests_execdir': installed_tests_execdir,
+}
+esm_test_description = configure_file(
+ configuration: esm_test_description_subst,
+ input: '../minijasmine-module.test.in', output: 'testESModules.test',
+ install: get_option('installed_tests'),
+ install_dir: installed_tests_metadir)
+
+if get_option('installed_tests')
+ install_data('testESModules.js', install_dir: installed_js_tests_dir)
+endif
diff --git a/installed-tests/js/modules/exports.js b/installed-tests/js/modules/exports.js
new file mode 100644
index 00000000..180151b9
--- /dev/null
+++ b/installed-tests/js/modules/exports.js
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+export default 5;
+
+export const NamedExport = 'Hello, World';
diff --git a/installed-tests/js/testESModules.js b/installed-tests/js/testESModules.js
new file mode 100644
index 00000000..744722f8
--- /dev/null
+++ b/installed-tests/js/testESModules.js
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import gi from 'gi';
+import system from 'system';
+
+import $ from 'resource:///org/gjs/jsunit/modules/exports.js';
+import {NamedExport} from 'resource:///org/gjs/jsunit/modules/exports.js';
+
+describe('ES module imports', function () {
+ it('default import', function () {
+ expect($).toEqual(5);
+ });
+
+ it('named import', function () {
+ expect(NamedExport).toEqual('Hello, World');
+ });
+
+ it('system import', function () {
+ expect(system.exit.toString()).toEqual('function exit() {\n [native code]\n}');
+ });
+
+ it('GObject introspection import', function () {
+ expect(gi.require('GObject').toString()).toEqual('[object GIRepositoryNamespace]');
+ });
+
+ it('import.meta.url', function () {
+ expect(import.meta.url).toMatch(/\/installed-tests\/js\/testESModules\.js$/);
+ });
+});
diff --git a/installed-tests/minijasmine-module.test.in b/installed-tests/minijasmine-module.test.in
new file mode 100644
index 00000000..e9e6b700
--- /dev/null
+++ b/installed-tests/minijasmine-module.test.in
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Philip Chimento <philip chimento gmail com>
+
+[Test]
+Type=session
+Exec=@installed_tests_execdir@/minijasmine @installed_tests_execdir@/js/@name@ -m
+Output=TAP
diff --git a/js.gresource.xml b/js.gresource.xml
index bdb6b665..c8e9f7cc 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -3,6 +3,14 @@
<!-- SPDX-FileCopyrightText: 2014 Red Hat, Inc. -->
<gresources>
<gresource prefix="/org/gnome/gjs">
+ <!-- Internal modules -->
+ <file>modules/internal/bootstrap/module.js</file>
+ <file>modules/internal/entry.js</file>
+ <file>modules/internal/modules/esm.js</file>
+ <file>modules/internal/modules/gi.js</file>
+
+ <!-- ESM-based modules -->
+ <file>modules/esm/gi.js</file>
<!-- Script-based Modules -->
<file>modules/script/_bootstrap/debugger.js</file>
<file>modules/script/_bootstrap/default.js</file>
diff --git a/meson.build b/meson.build
index b3ac224d..27a6cb9b 100644
--- a/meson.build
+++ b/meson.build
@@ -113,7 +113,7 @@ endif
### Check for required libraries ###############################################
-glib_required_version = '>= 2.58.0'
+glib_required_version = '>= 2.66.0'
glib = dependency('glib-2.0', version: glib_required_version,
fallback: ['glib', 'libglib_dep'])
gthread = dependency('gthread-2.0', version: glib_required_version,
@@ -402,6 +402,7 @@ libgjs_sources = [
'gjs/error-types.cpp',
'gjs/global.cpp', 'gjs/global.h',
'gjs/importer.cpp', 'gjs/importer.h',
+ 'gjs/internal.cpp', 'gjs/internal.h',
'gjs/mem.cpp', 'gjs/mem-private.h',
'gjs/module.cpp', 'gjs/module.h',
'gjs/native.cpp', 'gjs/native.h',
diff --git a/modules/esm/.eslintrc.yml b/modules/esm/.eslintrc.yml
new file mode 100644
index 00000000..d5d3ca16
--- /dev/null
+++ b/modules/esm/.eslintrc.yml
@@ -0,0 +1,7 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+extends: '../../.eslintrc.yml'
+parserOptions:
+ sourceType: 'module'
+ ecmaVersion: 2020
diff --git a/modules/esm/gi.js b/modules/esm/gi.js
new file mode 100644
index 00000000..a254cb56
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+const gi = import.meta.importSync('gi');
+
+const Gi = {
+ require(name, version = null) {
+ if (version !== null)
+ gi.versions[name] = version;
+
+ if (name === 'versions')
+ throw new Error('Cannot import namespace "versions", use the version parameter of Gi.require to
specify versions.');
+
+
+ return gi[name];
+ },
+};
+Object.freeze(Gi);
+
+export default Gi;
diff --git a/modules/internal/.eslintrc.yml b/modules/internal/.eslintrc.yml
new file mode 100644
index 00000000..d2edebf8
--- /dev/null
+++ b/modules/internal/.eslintrc.yml
@@ -0,0 +1,29 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+extends: ../../.eslintrc.yml
+parserOptions:
+ sourceType: 'module'
+ ecmaVersion: 2020
+globals:
+ ARGV: off
+ Debugger: readonly
+ GIRepositoryGType: off
+ imports: off
+ Intl: readonly
+ log: off
+ logError: off
+ print: off
+ printerr: off
+ moduleGlobalThis: readonly
+ compileModule: readonly
+ compileInternalModule: readonly
+ loadResourceOrFile: readonly
+ parseURI: readonly
+ uriExists: readonly
+ resolveRelativeResourceOrFile: readonly
+ setModuleResolveHook: readonly
+ setModuleMetaHook: readonly
+ setModuleLoadHook: readonly
+ setModulePrivate: readonly
+ getRegistry: readonly
diff --git a/modules/internal/bootstrap/module.js b/modules/internal/bootstrap/module.js
new file mode 100644
index 00000000..d6fad676
--- /dev/null
+++ b/modules/internal/bootstrap/module.js
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+/** @typedef {{ uri: string; scheme: string; host: string; path: string; query: Query }} Uri */
+
+/** @typedef {{ load(uri: Uri): [string, boolean]; }} SchemeHandler */
+/** @typedef {{ [key: string]: string | undefined; }} Query */
+
+/**
+ * Thrown when there is an error importing a module.
+ */
+export class ImportError extends Error {
+ /**
+ * @param {string | undefined} message the import error message
+ */
+ constructor(message) {
+ super(message);
+
+ this.name = 'ImportError';
+ }
+}
+
+/**
+ * ESModule is the "private" object of every module.
+ */
+export class ESModule {
+ /**
+ *
+ * @param {string} id the module's identifier
+ * @param {string} uri the module's URI
+ * @param {boolean} [internal] whether this module is "internal"
+ */
+ constructor(id, uri, internal = false) {
+ this.id = id;
+ this.uri = uri;
+ this.internal = internal;
+ }
+}
+
+/**
+ * Returns whether a string represents a relative path (e.g. ./, ../)
+ *
+ * @param {string} path a path to check if relative
+ * @returns {boolean}
+ */
+function isRelativePath(path) {
+ // Check if the path is relative.
+ return path.startsWith('./') || path.startsWith('../');
+}
+
+/**
+ * Handles resolving and loading URIs.
+ *
+ * @class
+ */
+export class ModuleLoader {
+ /**
+ * @param {typeof globalThis} global the global object to handle module resolution
+ */
+ constructor(global) {
+ this.global = global;
+ }
+
+ /**
+ * Loads a file or resource URI synchronously
+ *
+ * @param {Uri} uri the file or resource URI to load
+ * @returns {[string] | [string, boolean] | null}
+ */
+ loadURI(uri) {
+ if (uri.scheme === 'file' || uri.scheme === 'resource')
+ return [loadResourceOrFile(uri.uri)];
+
+ return null;
+ }
+
+ /**
+ * Resolves an import specifier given an optional parent importer.
+ *
+ * @param {string} specifier the import specifier
+ * @param {string | null} [parentURI] the URI of the module importing the specifier
+ * @returns {Uri | null}
+ */
+ resolveSpecifier(specifier, parentURI = null) {
+ try {
+ const uri = parseURI(specifier);
+
+ if (uri)
+ return uri;
+ } catch (err) {
+ // If it can't be parsed as a URI, try a relative path or return null.
+ }
+
+ if (isRelativePath(specifier)) {
+ if (!parentURI)
+ throw new ImportError('Cannot import from relative path when module path is unknown.');
+
+ return this.resolveRelativePath(specifier, parentURI);
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolves a path relative to a URI, throwing an ImportError if
+ * the parentURI isn't valid.
+ *
+ * @param {string} relativePath the relative path to resolve against the base URI
+ * @param {string} parentURI the parent URI
+ * @returns {Uri}
+ */
+ resolveRelativePath(relativePath, parentURI) {
+ // Ensure the parent URI is valid.
+ parseURI(parentURI);
+
+ // Handle relative imports from URI-based modules.
+ const relativeURI = resolveRelativeResourceOrFile(parentURI, relativePath);
+ if (!relativeURI)
+ throw new ImportError('File does not have a valid parent!');
+ return parseURI(relativeURI);
+ }
+
+ /**
+ * Compiles a module source text with the module's URI
+ *
+ * @param {ESModule} module a module private object
+ * @param {string} text the module source text to compile
+ * @returns {import("../types").Module}
+ */
+ compileModule(module, text) {
+ const compiled = compileInternalModule(module.uri, text);
+
+ setModulePrivate(compiled, module);
+
+ return compiled;
+ }
+
+ /**
+ * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+ * @param {string | null} parentURI the URI of the module triggering this resolve
+ *
+ * @returns {import("../types").Module | null}
+ */
+ resolveModule(specifier, parentURI) {
+ const registry = getRegistry(this.global);
+
+ // Check if the module has already been loaded
+ let module = registry.get(specifier);
+ if (module)
+ return module;
+
+ // 1) Resolve path and URI-based imports.
+ const uri = this.resolveSpecifier(specifier, parentURI);
+ if (uri) {
+ module = registry.get(uri.uri);
+
+ // Check if module is already loaded (relative handling)
+ if (module)
+ return module;
+
+ const result = this.loadURI(uri);
+ if (!result)
+ return null;
+
+ const [text, internal = false] = result;
+
+ const esmodule = new ESModule(uri.uri, uri.uri, internal);
+ const compiled = this.compileModule(esmodule, text);
+
+ if (!compiled)
+ throw new ImportError(`Failed to register module: ${uri}`);
+
+ registry.set(uri.uri, compiled);
+ return compiled;
+ }
+
+ return null;
+ }
+}
+
+export const internalModuleLoader = new ModuleLoader(globalThis);
+
+setModuleResolveHook(globalThis, (module, specifier) => {
+ const resolved = internalModuleLoader.resolveModule(specifier, module?.uri ?? null);
+ if (!resolved)
+ throw new ImportError(`Module not found: ${specifier}`);
+
+ return resolved;
+});
+
+setModuleMetaHook(globalThis, (module, meta) => {
+ meta.url = module.uri;
+});
+
+setModuleLoadHook(globalThis, (id, uri) => {
+ const m = new ESModule(id, uri);
+
+ const result = internalModuleLoader.loadURI(parseURI(uri));
+ if (!result)
+ throw new ImportError(`URI not found: ${uri}`);
+
+ const [text] = result;
+ const compiled = internalModuleLoader.compileModule(m, text);
+
+ const registry = getRegistry(globalThis);
+ registry.set(uri, compiled);
+
+ return compiled;
+});
diff --git a/modules/internal/entry.js b/modules/internal/entry.js
new file mode 100644
index 00000000..64ee7e06
--- /dev/null
+++ b/modules/internal/entry.js
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+// This file is called *after* ./bootstrap sets up internal module resolution.
+
+// Setup ES modules.
+import './modules/esm.js';
diff --git a/modules/internal/modules/esm.js b/modules/internal/modules/esm.js
new file mode 100644
index 00000000..a96707b5
--- /dev/null
+++ b/modules/internal/modules/esm.js
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import {ESModule, ImportError, ModuleLoader} from '../bootstrap/module.js';
+
+import {generateModule} from './gi.js';
+
+export class ESModuleLoader extends ModuleLoader {
+ /**
+ * @param {typeof globalThis} global the global object to register modules with.
+ */
+ constructor(global) {
+ super(global);
+
+ /**
+ * @type {Set<string>}
+ *
+ * The set of "module" URIs (the module search path)
+ */
+ this.moduleURIs = new Set();
+
+ /**
+ * @type {Map<string, import("../bootstrap/module.js").SchemeHandler>}
+ *
+ * A map of handlers for URI schemes (e.g. gi://)
+ */
+ this.schemeHandlers = new Map();
+ }
+
+ /**
+ * @param {ESModule} module a module private object
+ * @param {string} text the module source text
+ */
+ compileModule(module, text) {
+ const compiled = compileModule(module.uri, text);
+
+ setModulePrivate(compiled, module);
+
+ return compiled;
+ }
+
+ /**
+ * @param {string} specifier the package specifier
+ * @returns {string[]} the possible internal URIs
+ */
+ buildInternalURIs(specifier) {
+ const {moduleURIs} = this;
+ const builtURIs = [];
+
+ for (const uri of moduleURIs) {
+ const builtURI = `${uri}/${specifier}.js`;
+ builtURIs.push(builtURI);
+ }
+
+ return builtURIs;
+ }
+
+ /**
+ * @param {string} scheme the URI scheme to register
+ * @param {import("../bootstrap/module.js").SchemeHandler} handler a handler
+ */
+ registerScheme(scheme, handler) {
+ this.schemeHandlers.set(scheme, handler);
+ }
+
+ /**
+ * @param {import("../bootstrap/module.js").Uri} uri a Uri object to load
+ */
+ loadURI(uri) {
+ const {schemeHandlers} = this;
+
+ if (uri.scheme) {
+ const loader = schemeHandlers.get(uri.scheme);
+
+ if (loader)
+ return loader.load(uri);
+ }
+
+ const result = super.loadURI(uri);
+
+ if (result)
+ return result;
+
+ throw new ImportError(`Unable to load module from invalid URI: ${uri.uri}`);
+ }
+
+ /**
+ * Registers an internal resource URI as a bare-specifier root.
+ *
+ * For example, registering "resource:///org/gnome/gjs/modules/esm/" allows
+ * import "system" if "resource:///org/gnome/gjs/modules/esm/system.js"
+ * exists.
+ *
+ * @param {string} uri the URI to register.
+ */
+ registerModuleURI(uri) {
+ const {moduleURIs} = this;
+
+ moduleURIs.add(uri);
+ }
+
+ /**
+ * Resolves a module import with optional handling for relative imports.
+ *
+ * @param {string} specifier the module specifier to resolve for an import
+ * @param {string | null} moduleURI the importing module's URI or null if importing from the entry point
+ * @returns {import("../types").Module}
+ */
+ resolveModule(specifier, moduleURI) {
+ const module = super.resolveModule(specifier, moduleURI);
+ if (module)
+ return module;
+
+ // 2) Resolve internal imports.
+
+ const uri = this.buildInternalURIs(specifier).find(uriExists);
+
+ if (!uri)
+ throw new ImportError(`Attempted to load unregistered global module: ${specifier}`);
+
+ const parsed = parseURI(uri);
+ if (parsed.scheme !== 'file' && parsed.scheme !== 'resource')
+ throw new ImportError('Only file:// and resource:// URIs are currently supported.');
+
+ const text = loadResourceOrFile(parsed.uri);
+ const priv = new ESModule(specifier, uri, true);
+ const compiled = this.compileModule(priv, text);
+ if (!compiled)
+ throw new ImportError(`Failed to register module: ${uri}`);
+
+ const registry = getRegistry(this.global);
+ if (!registry.has(specifier))
+ registry.set(specifier, compiled);
+
+ return compiled;
+ }
+}
+
+export const moduleLoader = new ESModuleLoader(moduleGlobalThis);
+
+// Always let ESM-specific modules take priority over core modules.
+moduleLoader.registerModuleURI('resource:///org/gnome/gjs/modules/esm/');
+moduleLoader.registerModuleURI('resource:///org/gnome/gjs/modules/core/');
+
+const giVersionMap = new Map();
+
+giVersionMap.set('GLib', '2.0');
+giVersionMap.set('Gio', '2.0');
+giVersionMap.set('GObject', '2.0');
+
+/**
+ * @param {string} lib the GI namespace to get the version for.
+ */
+function getGIVersionMap(lib) {
+ return giVersionMap.get(lib);
+}
+
+moduleLoader.registerScheme('gi', {
+ /**
+ * @param {import("../bootstrap/module.js").Uri} uri the URI to load
+ */
+ load(uri) {
+ const version = uri.query.version ?? getGIVersionMap(uri.host);
+
+ if (version)
+ giVersionMap.set(uri.host, version);
+
+ return [generateModule(uri.host, version), true];
+ },
+});
+
+/**
+ * @param {ESModule} module
+ * @param {ImportMeta} meta
+ */
+setModuleMetaHook(moduleGlobalThis, (module, meta) => {
+ meta.url = module.uri;
+
+ if (module.internal)
+ meta.importSync = globalThis.importSync;
+});
+
+/**
+ * @param {string} id
+ * @param {string} uri
+ */
+setModuleLoadHook(moduleGlobalThis, (id, uri) => {
+ const priv = new ESModule(id, uri);
+
+ const [text] = moduleLoader.loadURI(parseURI(uri));
+ const compiled = moduleLoader.compileModule(priv, text);
+
+ const registry = getRegistry(moduleGlobalThis);
+
+ registry.set(id, compiled);
+
+ return compiled;
+});
+
+setModuleResolveHook(moduleGlobalThis, (module, specifier) => {
+ return moduleLoader.resolveModule(specifier, module.uri);
+});
+
diff --git a/modules/internal/modules/gi.js b/modules/internal/modules/gi.js
new file mode 100644
index 00000000..d38bd347
--- /dev/null
+++ b/modules/internal/modules/gi.js
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+/**
+ * Creates a module source text to expose a GI namespace via a default export.
+ *
+ * @param {string} namespace the GI namespace to import
+ * @param {string} [version] the version string of the namespace
+ *
+ * @returns {string} the generated module source text
+ */
+export function generateModule(namespace, version) {
+ const source = `
+ import $$gi from 'gi';
+
+ const $$ns = $$gi.require${version ? `('${namespace}', '${version}')` : `('${namespace}')`};
+
+ export default $$ns;
+ `;
+
+ return source;
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]