[gjs/esm/static-imports: 4/5] esm: Enable static module imports.
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/esm/static-imports: 4/5] esm: Enable static module imports.
- Date: Sun, 7 Feb 2021 03:51:35 +0000 (UTC)
commit 6f8b3cb39bc3995706e3d092e4733b8f53b5344d
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: tests, moving file operations into
internal.cpp, store module loader in global, some renames, some added
comments)
.reuse/dep5 | 1 +
doc/ESModules.md | 225 ++++++++++++++
doc/Modules.md | 35 ++-
gjs/atoms.h | 4 +
gjs/context-private.h | 4 +
gjs/context.cpp | 186 +++++++++---
gjs/global.cpp | 80 +++++
gjs/global.h | 11 +
gjs/internal.cpp | 461 +++++++++++++++++++++++++++++
gjs/internal.h | 54 ++++
gjs/jsapi-util.cpp | 18 ++
gjs/jsapi-util.h | 2 +
gjs/module.cpp | 215 ++++++++++++++
gjs/module.h | 15 +
installed-tests/js/.eslintrc.yml | 7 +
installed-tests/js/jsunit.gresources.xml | 3 +
installed-tests/js/meson.build | 19 ++
installed-tests/js/modules/data.txt | 1 +
installed-tests/js/modules/exports.js | 12 +
installed-tests/js/modules/importmeta.js | 7 +
installed-tests/js/testESModules.js | 56 ++++
installed-tests/minijasmine-module.test.in | 7 +
installed-tests/scripts/testCommandLine.sh | 11 +-
js.gresource.xml | 6 +
meson.build | 3 +-
modules/esm/.eslintrc.yml | 7 +
modules/esm/gi.js | 25 ++
modules/internal/.eslintrc.yml | 27 ++
modules/internal/internalLoader.js | 229 ++++++++++++++
modules/internal/loader.js | 170 +++++++++++
test/gjs-tests.cpp | 167 +++++++++++
test/mock-js-resources.gresource.xml | 5 +
test/modules/.eslintrc.yml | 5 +
test/modules/default.js | 7 +
test/modules/exit.js | 5 +
test/modules/exit0.js | 5 +
test/modules/import.js | 6 +
test/modules/throws.js | 4 +
38 files changed, 2061 insertions(+), 44 deletions(-)
---
diff --git a/.reuse/dep5 b/.reuse/dep5
index 7a10a45b..7c50dc32 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -2,6 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files: .gitlab/issue_templates/*
doc/* CONTRIBUTING.md NEWS *README* tools/heapgraph.md tools/yarn.lock
+ installed-tests/js/modules/data.txt
Copyright: No rights reserved
License: CC0-1.0
diff --git a/doc/ESModules.md b/doc/ESModules.md
new file mode 100644
index 00000000..80017a37
--- /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..6400cbed 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,26 @@ 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.
+This function is only intended to be used when you want to import a
+library conditionally, since top-level import statements are resolved
+statically.
+
+### 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 +310,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/atoms.h b/gjs/atoms.h
index 966999c8..7fe061f1 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -35,9 +35,11 @@ class JSTracer;
macro(gtype, "$gtype") \
macro(height, "height") \
macro(imports, "imports") \
+ macro(importSync, "importSync") \
macro(init, "_init") \
macro(instance_init, "_instance_init") \
macro(interact, "interact") \
+ macro(internal, "internal") \
macro(length, "length") \
macro(line_number, "lineNumber") \
macro(message, "message") \
@@ -56,6 +58,8 @@ class JSTracer;
macro(signal_id, "signalId") \
macro(stack, "stack") \
macro(to_string, "toString") \
+ macro(uri, "uri") \
+ macro(url, "url") \
macro(value_of, "valueOf") \
macro(version, "version") \
macro(versions, "versions") \
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..c3810d97 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, "internalLoader")) {
+ gjs_log_exception(cx);
+ g_error("Failed to load internal module loaders.");
+ }
+
+ JS::RootedObject loader(
+ cx, gjs_module_load(
+ cx, "resource:///org/gnome/gjs/modules/internal/loader.js",
+ "resource:///org/gnome/gjs/modules/internal/loader.js"));
+
+ if (!loader) {
+ gjs_log_exception(cx);
+ g_error("Failed to load module loader module.");
+ }
+
+ if (!JS::ModuleInstantiate(cx, loader)) {
+ gjs_log_exception(cx);
+ g_error("Failed to instantiate module loader module.");
+ }
+
+ if (!JS::ModuleEvaluate(cx, loader)) {
+ gjs_log_exception(cx);
+ g_error("Failed to evaluate module loader module.");
}
}
@@ -1033,25 +1100,78 @@ 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);
- g_error(
- "GjsContextPrivate::eval_module(%s) is not implemented. Exiting with "
- "error.",
- identifier);
+ bool auto_profile = auto_profile_enter();
- return false;
+ JSAutoRealm ac(m_cx, m_global);
+
+ 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 load module with identifier: '%s'", identifier);
+ *exit_status_p = 1;
+ 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);
+ *exit_status_p = 1;
+ 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..6e0dac3b 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,66 @@ 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_get_registry, 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("setGlobalModuleLoader", gjs_internal_set_global_module_loader, 2,
+ 0),
+ JS_FN("setModulePrivate", gjs_internal_set_module_private, 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 +341,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 +354,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 +508,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 +532,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..569a8ce1 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,10 @@ enum class GjsDebuggerGlobalSlot : uint32_t {
enum class GjsGlobalSlot : uint32_t {
IMPORTS = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+ // Stores an object with methods to resolve and load modules
+ MODULE_LOADER,
+ // Stores the module registry (a Map object)
+ MODULE_REGISTRY,
NATIVE_REGISTRY,
PROTOTYPE_gtype,
PROTOTYPE_importer,
@@ -58,6 +63,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 +98,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 +108,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..184f2457
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,461 @@
+// 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));
+
+ gjs_debug(GJS_DEBUG_IMPORTER, "Loading internal module '%s' (%s)",
+ identifier, full_path.get());
+
+ 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;
+}
+
+/**
+ * gjs_internal_set_global_module_loader:
+ *
+ * @brief Sets the MODULE_LOADER slot of the passed global object.
+ * The second argument should be an instance of ModuleLoader or
+ * InternalModuleLoader. Its moduleResolveHook and moduleLoadHook properties
+ * will be called.
+ *
+ * @returns guaranteed to return true or assert.
+ */
+bool gjs_internal_set_global_module_loader(JSContext*, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 2 && "setGlobalModuleLoader takes 2 arguments");
+
+ JS::Value v_global = args[0];
+ JS::Value v_loader = args[1];
+
+ g_assert(v_global.isObject() && "first argument must be an object");
+ g_assert(v_loader.isObject() && "second argument must be an object");
+
+ gjs_set_global_slot(&v_global.toObject(), GjsGlobalSlot::MODULE_LOADER,
+ v_loader);
+
+ args.rval().setUndefined();
+ 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_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_get_registry(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ g_assert(args.length() == 1 && "getRegistry takes 1 argument");
+ 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) {
+ gjs_throw_custom(cx, JSProto_Error, "ImportError",
+ "Attempted to import invalid URI: %s (%s)", uri.get(),
+ error->message);
+ g_clear_error(&error);
+ return false;
+ }
+
+ 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) {
+ gjs_throw_custom(cx, JSProto_Error, "ImportError",
+ "Attempted to import invalid URI: %s (%s)",
+ uri.get(), error->message);
+ g_clear_error(&error);
+ return false;
+ }
+
+ 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)) {
+ gjs_throw_custom(cx, JSProto_Error, "ImportError",
+ "Unable to load file from: %s (%s)", uri.get(),
+ error->message);
+ g_clear_error(&error);
+ return false;
+ }
+
+ 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..212e5458
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,54 @@
+// 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_get_registry(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_set_global_module_loader(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_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..c810f7c9 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,22 +13,31 @@
#include <gio/gio.h>
#include <glib.h>
+#include <js/CallArgs.h>
+#include <js/CharacterEncoding.h> // for ConstUTF8CharsZ
#include <js/Class.h>
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
+#include <js/Conversions.h>
#include <js/GCVector.h> // for RootedVector
+#include <js/Id.h>
#include <js/PropertyDescriptor.h>
#include <js/RootingAPI.h>
#include <js/SourceText.h>
#include <js/TypeDecls.h>
+#include <js/Utility.h> // for UniqueChars
#include <js/Value.h>
+#include <js/ValueArray.h>
#include <jsapi.h> // for JS_DefinePropertyById, ...
+#include "gjs/atoms.h"
#include "gjs/context-private.h"
#include "gjs/global.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"
class GjsScriptModule {
@@ -269,3 +279,208 @@ 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.
+ *
+ * @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 v_loader(
+ cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
+ g_assert(v_loader.isObject());
+ JS::RootedObject loader(cx, &v_loader.toObject());
+
+ 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);
+
+ gjs_debug(GJS_DEBUG_IMPORTER,
+ "Module resolve hook for module '%s' (%s), global %p", identifier,
+ file_uri, global.get());
+
+ JS::RootedValue result(cx);
+ if (!JS::Call(cx, loader, "moduleLoadHook", args, &result))
+ return nullptr;
+
+ g_assert(result.isObject() && "Module hook failed to return an object!");
+ return &result.toObject();
+}
+
+/**
+ * import_native_module_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.
+ *
+ * Compare gjs_import_native_module() for the legacy importer.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while importing the native module.
+ */
+static bool import_native_module_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_populate_module_meta:
+ *
+ * Hook SpiderMonkey calls to populate the import.meta object.
+ * Defines a property "import.meta.url", and additionally a method
+ * "import.meta.importSync" if this is an internal module.
+ *
+ * @param private_ref the private value for the #Module object
+ * @param meta 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) {
+ g_assert(private_ref.isObject());
+ JS::RootedObject module(cx, &private_ref.toObject());
+
+ gjs_debug(GJS_DEBUG_IMPORTER, "Module metadata hook for module %p",
+ &private_ref.toObject());
+
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+ JS::RootedValue v_uri(cx);
+ if (!JS_GetPropertyById(cx, module, atoms.uri(), &v_uri) ||
+ !JS_DefinePropertyById(cx, meta, atoms.url(), v_uri,
+ GJS_MODULE_PROP_FLAGS))
+ return false;
+
+ JS::RootedValue v_internal(cx);
+ if (!JS_GetPropertyById(cx, module, atoms.internal(), &v_internal))
+ return false;
+ if (JS::ToBoolean(v_internal)) {
+ gjs_debug(GJS_DEBUG_IMPORTER, "Defining meta.importSync for module %p",
+ &private_ref.toObject());
+ if (!JS_DefineFunctionById(cx, meta, atoms.importSync(),
+ import_native_module_sync, 1,
+ GJS_MODULE_PROP_FLAGS))
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * gjs_module_resolve:
+ *
+ * Hook SpiderMonkey calls to resolve import specifiers.
+ *
+ * @param importingModulePriv the private value of the #Module object initiating
+ * the import.
+ * @param specifier the import specifier to resolve
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
+ 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.");
+ g_assert(importingModulePriv.isObject() &&
+ "the importing module can't be null, don't add import to the "
+ "bootstrap script");
+
+ JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+ JS::RootedValue v_loader(
+ cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
+ g_assert(v_loader.isObject());
+ JS::RootedObject loader(cx, &v_loader.toObject());
+
+ JS::RootedValueArray<2> args(cx);
+ args[0].set(importingModulePriv);
+ args[1].setString(specifier);
+
+ gjs_debug(GJS_DEBUG_IMPORTER,
+ "Module resolve hook for module '%s' (relative to %p), global %p",
+ gjs_debug_string(specifier).c_str(),
+ &importingModulePriv.toObject(), global.get());
+
+ JS::RootedValue result(cx);
+ if (!JS::Call(cx, loader, "moduleResolveHook", args, &result))
+ return nullptr;
+
+ g_assert(result.isObject() && "resolve hook failed to return an object!");
+ 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..7f329a58 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -29,3 +29,10 @@ globals:
clearTimeout: writable
setInterval: writable
setTimeout: writable
+overrides:
+ - files:
+ - testESModules.js
+ - modules/importmeta.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..3e100b1b 100644
--- a/installed-tests/js/jsunit.gresources.xml
+++ b/installed-tests/js/jsunit.gresources.xml
@@ -12,6 +12,9 @@
<file>modules/badOverrides/Gio.js</file>
<file>modules/badOverrides/Regress.js</file>
<file>modules/badOverrides/WarnLib.js</file>
+ <file>modules/data.txt</file>
+ <file>modules/importmeta.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/data.txt b/installed-tests/js/modules/data.txt
new file mode 100644
index 00000000..082b3465
--- /dev/null
+++ b/installed-tests/js/modules/data.txt
@@ -0,0 +1 @@
+test data
diff --git a/installed-tests/js/modules/exports.js b/installed-tests/js/modules/exports.js
new file mode 100644
index 00000000..26e76c17
--- /dev/null
+++ b/installed-tests/js/modules/exports.js
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import Gio from 'gi://Gio';
+
+export default 5;
+
+export const NamedExport = 'Hello, World';
+
+const thisFile = Gio.File.new_for_uri(import.meta.url);
+const dataFile = thisFile.get_parent().resolve_relative_path('data.txt');
+export const [, data] = dataFile.load_contents(null);
diff --git a/installed-tests/js/modules/importmeta.js b/installed-tests/js/modules/importmeta.js
new file mode 100644
index 00000000..f1c7e402
--- /dev/null
+++ b/installed-tests/js/modules/importmeta.js
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+if (typeof import.meta.importSync !== 'undefined')
+ throw new Error('internal import meta property should not be visible in userland');
+
+export default Object.keys(import.meta);
diff --git a/installed-tests/js/testESModules.js b/installed-tests/js/testESModules.js
new file mode 100644
index 00000000..ea551485
--- /dev/null
+++ b/installed-tests/js/testESModules.js
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import gi from 'gi';
+import Gio from 'gi://Gio';
+
+import $ from 'resource:///org/gjs/jsunit/modules/exports.js';
+import {NamedExport, data} from 'resource:///org/gjs/jsunit/modules/exports.js';
+import metaProperties from 'resource:///org/gjs/jsunit/modules/importmeta.js';
+
+describe('ES module imports', function () {
+ it('default import', function () {
+ expect($).toEqual(5);
+ });
+
+ it('named import', function () {
+ expect(NamedExport).toEqual('Hello, World');
+ });
+
+ it('GObject introspection import', function () {
+ expect(gi.require('GObject').toString()).toEqual('[object GIRepositoryNamespace]');
+ });
+
+ it('import with version parameter', function () {
+ expect(gi.require('GObject', '2.0')).toBe(gi.require('GObject'));
+ });
+
+ it('import again with other version parameter', function () {
+ expect(() => gi.require('GObject', '1.75')).toThrow();
+ });
+
+ it('import for the first time with wrong version', function () {
+ expect(() => gi.require('Gtk', '1.75')).toThrow();
+ });
+
+ it('import nonexistent module', function () {
+ expect(() => gi.require('PLib')).toThrow();
+ });
+
+ it('GObject introspection import via URL scheme', function () {
+ expect(Gio.toString()).toEqual('[object GIRepositoryNamespace]');
+ });
+
+ it('import.meta.url', function () {
+ expect(import.meta.url).toMatch(/\/installed-tests\/js\/testESModules\.js$/);
+ });
+
+ it('finds files relative to import.meta.url', function () {
+ // this data is loaded inside exports.js relative to import.meta.url
+ expect(data).toEqual(Uint8Array.from('test data\n', c => c.codePointAt()));
+ });
+
+ it('does not expose internal import.meta properties to userland modules', function () {
+ expect(metaProperties).toEqual(['url']);
+ });
+});
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/installed-tests/scripts/testCommandLine.sh b/installed-tests/scripts/testCommandLine.sh
index 2d679e72..539e6929 100755
--- a/installed-tests/scripts/testCommandLine.sh
+++ b/installed-tests/scripts/testCommandLine.sh
@@ -59,6 +59,12 @@ async function bar() {
bar();
EOF
+# this JS script should fail to import a second version of the same namespace
+cat <<EOF >doublegi.js
+import 'gi://Gio?version=2.0';
+import 'gi://Gio?version=75.94';
+EOF
+
total=0
report () {
@@ -256,6 +262,9 @@ grep -q TN: coverage.lcov
report "coverage prefix is treated as an absolute path"
rm -f coverage.lcov
-rm -f exit.js help.js promise.js awaitcatch.js
+$gjs -m doublegi.js 2>&1 | grep -q 'already loaded'
+report "avoid statically importing two versions of the same module"
+
+rm -f exit.js help.js promise.js awaitcatch.js doublegi.js
echo "1..$total"
diff --git a/js.gresource.xml b/js.gresource.xml
index bdb6b665..61193ab0 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -3,6 +3,12 @@
<!-- SPDX-FileCopyrightText: 2014 Red Hat, Inc. -->
<gresources>
<gresource prefix="/org/gnome/gjs">
+ <!-- Internal modules -->
+ <file>modules/internal/internalLoader.js</file>
+ <file>modules/internal/loader.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..3b2a5c4c
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,25 @@
+// 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(namespace, version = undefined) {
+ if (namespace === 'versions')
+ throw new Error('Cannot import namespace "versions", use the version parameter of Gi.require to
specify versions.');
+
+ if (version !== undefined) {
+ const alreadyLoadedVersion = gi.versions[namespace];
+ if (alreadyLoadedVersion !== undefined && version !== alreadyLoadedVersion) {
+ throw new Error(`Version ${alreadyLoadedVersion} of GI module ${
+ namespace} already loaded, cannot load version ${version}`);
+ }
+ gi.versions[namespace] = version;
+ }
+
+ return gi[namespace];
+ },
+};
+Object.freeze(Gi);
+
+export default Gi;
diff --git a/modules/internal/.eslintrc.yml b/modules/internal/.eslintrc.yml
new file mode 100644
index 00000000..b06bc212
--- /dev/null
+++ b/modules/internal/.eslintrc.yml
@@ -0,0 +1,27 @@
+---
+# 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
+ setGlobalModuleLoader: readonly
+ setModulePrivate: readonly
+ getRegistry: readonly
diff --git a/modules/internal/internalLoader.js b/modules/internal/internalLoader.js
new file mode 100644
index 00000000..b1023132
--- /dev/null
+++ b/modules/internal/internalLoader.js
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+// InternalModuleLoader is the mechanism present on the internal global object
+// which resolves (translates a module specifier such as './mymod.js' or 'gi' to
+// an actual file) and loads internal modules.
+// It is used to bootstrap the actual module loader in loader.js, and also
+// serves as its base class.
+
+/** @typedef {{ uri: string; scheme: string; host: string; path: string; query: Query }} Uri */
+
+/**
+ * Use '__internal: never' to prevent any object from being type compatible with Module
+ * because it is an internal type.
+ *
+ * @typedef {{__internal: never;}} Module
+ */
+/** @typedef {typeof moduleGlobalThis | typeof globalThis} Global */
+/** @typedef {{ load(uri: Uri): [contents: string, internal: boolean]; }} SchemeHandler */
+/** @typedef {{ [key: string]: string | undefined; }} Query */
+/** @typedef {(uri: string, contents: string) => Module} CompileFunc */
+
+/**
+ * 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';
+ }
+}
+
+/**
+ * ModulePrivate is the "private" object of every module.
+ */
+export class ModulePrivate {
+ /**
+ *
+ * @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. Note that this doesn't mean "relative
+ // path" in the GLib sense, as in "not absolute" — it means a relative path
+ // module specifier, which must start with a '.' or '..' path component.
+ return path.startsWith('./') || path.startsWith('../');
+}
+
+/**
+ * Handles resolving and loading URIs.
+ *
+ * @class
+ */
+export class InternalModuleLoader {
+ /**
+ * @param {typeof globalThis} global the global object to handle module
+ * resolution
+ * @param {(string, string) => import("../types").Module} compileFunc the
+ * function to compile a source into a module for a particular global
+ * object. Should be compileInternalModule() for InternalModuleLoader,
+ * but overridden in ModuleLoader
+ */
+ constructor(global, compileFunc) {
+ this.global = global;
+ this.compileFunc = compileFunc;
+ }
+
+ /**
+ * Loads a file or resource URI synchronously
+ *
+ * @param {Uri} uri the file or resource URI to load
+ * @returns {[contents: string, internal?: 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 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} importingModuleURI the URI of the module triggering this
+ * resolve
+ * @returns {Uri}
+ */
+ resolveRelativePath(relativePath, importingModuleURI) {
+ // Ensure the parent URI is valid.
+ parseURI(importingModuleURI);
+
+ // Handle relative imports from URI-based modules.
+ const relativeURI = resolveRelativeResourceOrFile(importingModuleURI, 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 {ModulePrivate} priv a module private object
+ * @param {string} text the module source text to compile
+ * @returns {Module}
+ */
+ compileModule(priv, text) {
+ const compiled = this.compileFunc(priv.uri, text);
+
+ setModulePrivate(compiled, priv);
+
+ return compiled;
+ }
+
+ /**
+ * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+ * @param {string | null} importingModuleURI the URI of the module
+ * triggering this resolve
+ *
+ * @returns {Module | null}
+ */
+ resolveModule(specifier, importingModuleURI) {
+ 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, importingModuleURI);
+ 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 priv = new ModulePrivate(uri.uri, uri.uri, internal);
+ const compiled = this.compileModule(priv, text);
+
+ registry.set(uri.uri, compiled);
+ return compiled;
+ }
+
+ return null;
+ }
+
+ moduleResolveHook(importingModulePriv, specifier) {
+ const resolved = this.resolveModule(specifier, importingModulePriv.uri ?? null);
+ if (!resolved)
+ throw new ImportError(`Module not found: ${specifier}`);
+
+ return resolved;
+ }
+
+ moduleLoadHook(id, uri) {
+ const priv = new ModulePrivate(id, uri);
+
+ const result = this.loadURI(parseURI(uri));
+ // result can only be null if `this` is InternalModuleLoader. If `this`
+ // is ModuleLoader, then loadURI() will have thrown
+ if (!result)
+ throw new ImportError(`URI not found: ${uri}`);
+
+ const [text] = result;
+ const compiled = this.compileModule(priv, text);
+
+ const registry = getRegistry(this.global);
+ registry.set(id, compiled);
+
+ return compiled;
+ }
+}
+
+export const internalModuleLoader = new InternalModuleLoader(globalThis, compileInternalModule);
+setGlobalModuleLoader(globalThis, internalModuleLoader);
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
new file mode 100644
index 00000000..fde96eb0
--- /dev/null
+++ b/modules/internal/loader.js
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import {ImportError, InternalModuleLoader, ModulePrivate} from './internalLoader.js';
+
+class ModuleLoader extends InternalModuleLoader {
+ /**
+ * @param {typeof moduleGlobalThis} global the global object to register modules with.
+ */
+ constructor(global) {
+ // Sets 'compileFunc' in InternalModuleLoader to be 'compileModule'
+ super(global, compileModule);
+
+ /**
+ * @type {Set<string>}
+ *
+ * The set of "module" URIs (the module search path)
+ * For example, having "resource:///org/gnome/gjs/modules/esm/" in this
+ * set allows import "system" if
+ * "resource:///org/gnome/gjs/modules/esm/system.js" exists.
+ */
+ this.moduleURIs = new Set([
+ 'resource:///org/gnome/gjs/modules/esm/',
+ ]);
+
+ /**
+ * @type {Map<string, import("./internalLoader.js").SchemeHandler>}
+ *
+ * A map of handlers for URI schemes (e.g. gi://)
+ */
+ this.schemeHandlers = new Map();
+ }
+
+ /**
+ * @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("./internalLoader.js").SchemeHandler} handler a handler
+ */
+ registerScheme(scheme, handler) {
+ this.schemeHandlers.set(scheme, handler);
+ }
+
+ /**
+ * Overrides InternalModuleLoader.loadURI
+ *
+ * @param {import("./internalLoader.js").Uri} uri a Uri object to load
+ */
+ loadURI(uri) {
+ if (uri.scheme) {
+ const loader = this.schemeHandlers.get(uri.scheme);
+
+ if (loader)
+ return loader.load(uri);
+ }
+
+ const result = super.loadURI(uri);
+
+ if (result)
+ return result;
+
+ throw new ImportError(`Invalid module URI: ${uri.uri}`);
+ }
+
+ /**
+ * Resolves a bare specifier like 'system' against internal resources,
+ * erroring if no resource is found.
+ *
+ * @param {string} specifier the module specifier to resolve for an import
+ * @returns {import("./internalLoader").Module}
+ */
+ resolveBareSpecifier(specifier) {
+ // 2) Resolve internal imports.
+
+ const uri = this.buildInternalURIs(specifier).find(uriExists);
+
+ if (!uri)
+ throw new ImportError(`Unknown 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 ModulePrivate(specifier, uri, true);
+ const compiled = this.compileModule(priv, text);
+
+ const registry = getRegistry(this.global);
+ if (!registry.has(specifier))
+ registry.set(specifier, compiled);
+
+ return compiled;
+ }
+
+ /**
+ * Resolves a module import with optional handling for relative imports.
+ * Overrides InternalModuleLoader.moduleResolveHook
+ *
+ * @param {import("./internalLoader.js").ModulePrivate} importingModulePriv
+ * the private object of the module initiating the import
+ * @param {string} specifier the module specifier to resolve for an import
+ * @returns {import("./internalLoader").Module}
+ */
+ moduleResolveHook(importingModulePriv, specifier) {
+ const module = this.resolveModule(specifier, importingModulePriv.uri);
+ if (module)
+ return module;
+
+ return this.resolveBareSpecifier(specifier);
+ }
+}
+
+const moduleLoader = new ModuleLoader(moduleGlobalThis);
+setGlobalModuleLoader(moduleGlobalThis, moduleLoader);
+
+const giVersionMap = new Map([
+ ['GLib', '2.0'],
+ ['Gio', '2.0'],
+ ['GObject', '2.0'],
+]);
+
+/**
+ * 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
+ */
+function generateGIModule(namespace, version) {
+ return `
+ import $$gi from 'gi';
+ export default $$gi.require('${namespace}'${version !== undefined ? `, '${version}'` : ''});
+ `;
+}
+
+moduleLoader.registerScheme('gi', {
+ /**
+ * @param {import("./internalLoader.js").Uri} uri the URI to load
+ */
+ load(uri) {
+ const namespace = uri.host;
+ const alreadyLoadedVersion = giVersionMap.get(namespace);
+ const version = uri.query.version ?? alreadyLoadedVersion;
+
+ if (version) {
+ if (alreadyLoadedVersion !== undefined && version !== alreadyLoadedVersion) {
+ throw new ImportError(`Version ${alreadyLoadedVersion} of GI module ${
+ namespace} already loaded, cannot load version ${version}`);
+ }
+ giVersionMap.set(namespace, version);
+ }
+
+ return [generateGIModule(namespace, version), true];
+ },
+});
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index c7555013..cd79d100 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -150,6 +150,155 @@ gjstest_test_func_gjs_context_exit(void)
g_object_unref(context);
}
+static void gjstest_test_func_gjs_context_eval_module_file() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ uint8_t exit_status;
+ GError* error = nullptr;
+
+ bool ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/default.js",
+ &exit_status, &error);
+
+ g_assert_true(ok);
+ g_assert_no_error(error);
+ // for modules, last executed statement is _not_ the exit code
+ g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_throw() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ uint8_t exit_status;
+ GError* error = nullptr;
+
+ g_test_expect_message("Gjs", G_LOG_LEVEL_CRITICAL, "*bad module*");
+
+ bool ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/throws.js",
+ &exit_status, &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+ g_assert_cmpuint(exit_status, ==, 1);
+
+ g_test_assert_expected_messages();
+
+ g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_exit() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+ uint8_t exit_status;
+
+ bool ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/exit0.js",
+ &exit_status, &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT);
+ g_assert_cmpuint(exit_status, ==, 0);
+
+ g_clear_error(&error);
+
+ ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/exit.js",
+ &exit_status, &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT);
+ g_assert_cmpuint(exit_status, ==, 42);
+
+ g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_fail_instantiate() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+ uint8_t exit_status;
+
+ g_test_expect_message("Gjs", G_LOG_LEVEL_WARNING, "*foo*");
+
+ // evaluating this module without registering 'foo' first should make it
+ // fail ModuleInstantiate
+ bool ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/import.js",
+ &exit_status, &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+ g_assert_cmpuint(exit_status, ==, 1);
+
+ g_test_assert_expected_messages();
+
+ g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_register_module_eval_module() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+
+ bool ok = gjs_context_register_module(
+ gjs, "foo", "resource:///org/gnome/gjs/mock/test/modules/default.js",
+ &error);
+
+ g_assert_true(ok);
+ g_assert_no_error(error);
+
+ uint8_t exit_status;
+ ok = gjs_context_eval_module(gjs, "foo", &exit_status, &error);
+
+ g_assert_true(ok);
+ g_assert_no_error(error);
+ g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_register_module_eval_module_file() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+
+ bool ok = gjs_context_register_module(
+ gjs, "foo", "resource:///org/gnome/gjs/mock/test/modules/default.js",
+ &error);
+
+ g_assert_true(ok);
+ g_assert_no_error(error);
+
+ uint8_t exit_status;
+ ok = gjs_context_eval_module_file(
+ gjs, "resource:///org/gnome/gjs/mock/test/modules/import.js",
+ &exit_status, &error);
+
+ g_assert_true(ok);
+ g_assert_no_error(error);
+ g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_register_module_non_existent() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+
+ bool ok = gjs_context_register_module(gjs, "foo", "nonexist.js", &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+
+ g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_unregistered() {
+ GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+ GError* error = nullptr;
+ uint8_t exit_status;
+
+ bool ok = gjs_context_eval_module(gjs, "foo", &exit_status, &error);
+
+ g_assert_false(ok);
+ g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+ g_assert_cmpuint(exit_status, ==, 1);
+
+ g_clear_error(&error);
+}
+
#define JS_CLASS "\
const GObject = imports.gi.GObject; \
const FooBar = GObject.registerClass(class FooBar extends GObject.Object {}); \
@@ -619,6 +768,24 @@ main(int argc,
g_test_add_func("/gjs/context/eval/non-zero-terminated",
gjstest_test_func_gjs_context_eval_non_zero_terminated);
g_test_add_func("/gjs/context/exit", gjstest_test_func_gjs_context_exit);
+ g_test_add_func("/gjs/context/eval-module-file",
+ gjstest_test_func_gjs_context_eval_module_file);
+ g_test_add_func("/gjs/context/eval-module-file/throw",
+ gjstest_test_func_gjs_context_eval_module_file_throw);
+ g_test_add_func("/gjs/context/eval-module-file/exit",
+ gjstest_test_func_gjs_context_eval_module_file_exit);
+ g_test_add_func(
+ "/gjs/context/eval-module-file/fail-instantiate",
+ gjstest_test_func_gjs_context_eval_module_file_fail_instantiate);
+ g_test_add_func("/gjs/context/register-module/eval-module",
+ gjstest_test_func_gjs_context_register_module_eval_module);
+ g_test_add_func(
+ "/gjs/context/register-module/eval-module-file",
+ gjstest_test_func_gjs_context_register_module_eval_module_file);
+ g_test_add_func("/gjs/context/register-module/non-existent",
+ gjstest_test_func_gjs_context_register_module_non_existent);
+ g_test_add_func("/gjs/context/eval-module/unregistered",
+ gjstest_test_func_gjs_context_eval_module_unregistered);
g_test_add_func("/gjs/gobject/js_defined_type", gjstest_test_func_gjs_gobject_js_defined_type);
g_test_add_func("/gjs/gobject/without_introspection",
gjstest_test_func_gjs_gobject_without_introspection);
diff --git a/test/mock-js-resources.gresource.xml b/test/mock-js-resources.gresource.xml
index 23172772..e706563b 100644
--- a/test/mock-js-resources.gresource.xml
+++ b/test/mock-js-resources.gresource.xml
@@ -4,5 +4,10 @@
<gresources>
<gresource prefix="/org/gnome/gjs/mock">
<file>test/gjs-test-coverage/loadedJSFromResource.js</file>
+ <file>test/modules/default.js</file>
+ <file>test/modules/exit.js</file>
+ <file>test/modules/exit0.js</file>
+ <file>test/modules/import.js</file>
+ <file>test/modules/throws.js</file>
</gresource>
</gresources>
diff --git a/test/modules/.eslintrc.yml b/test/modules/.eslintrc.yml
new file mode 100644
index 00000000..b2391da9
--- /dev/null
+++ b/test/modules/.eslintrc.yml
@@ -0,0 +1,5 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+parserOptions:
+ sourceType: module
diff --git a/test/modules/default.js b/test/modules/default.js
new file mode 100644
index 00000000..56db7514
--- /dev/null
+++ b/test/modules/default.js
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+export default 77;
+
+// eslint-disable-next-line no-unused-expressions
+77;
diff --git a/test/modules/exit.js b/test/modules/exit.js
new file mode 100644
index 00000000..e02c3b34
--- /dev/null
+++ b/test/modules/exit.js
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import System from 'system';
+System.exit(42);
diff --git a/test/modules/exit0.js b/test/modules/exit0.js
new file mode 100644
index 00000000..bf16b3d1
--- /dev/null
+++ b/test/modules/exit0.js
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import System from 'system';
+System.exit(0);
diff --git a/test/modules/import.js b/test/modules/import.js
new file mode 100644
index 00000000..4b52f9d0
--- /dev/null
+++ b/test/modules/import.js
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import num from 'foo';
+if (num !== 77)
+ throw new Error('wrong number');
diff --git a/test/modules/throws.js b/test/modules/throws.js
new file mode 100644
index 00000000..2dce689d
--- /dev/null
+++ b/test/modules/throws.js
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+throw new Error('bad module');
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]