[gjs/esm/static-imports: 5/6] esm: Enable ESM static imports
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/esm/static-imports: 5/6] esm: Enable ESM static imports
- Date: Tue, 23 Jun 2020 00:56:54 +0000 (UTC)
commit c40bf15ebd6ae67adde40b8f90d8190799d4fe26
Author: Evan Welsh <noreply evanwelsh com>
Date: Mon Jun 22 19:55:04 2020 -0500
esm: Enable ESM static imports
gi/repo.cpp | 39 ++++
gjs/.eslintrc.yml | 22 +++
gjs/atoms.h | 1 +
gjs/console.cpp | 8 +-
gjs/context-private.h | 7 +-
gjs/context.cpp | 214 +++++++++++++++++++--
gjs/global.cpp | 179 +++++++++++++++++-
gjs/global.h | 7 +
gjs/internal.cpp | 452 +++++++++++++++++++++++++++++++++++++++++++++
gjs/internal.h | 66 +++++++
gjs/internal/errorTypes.js | 31 ++++
gjs/internal/module.js | 259 ++++++++++++++++++++++++++
gjs/jsapi-util.cpp | 4 +
gjs/jsapi-util.h | 3 +
gjs/module.cpp | 127 +++++++++++++
gjs/module.h | 42 ++++-
js.gresource.xml | 9 +
meson.build | 1 +
modules/esm/.eslintrc.yml | 4 +
modules/esm/gi.js | 14 ++
modules/esm/system.js | 1 +
21 files changed, 1460 insertions(+), 30 deletions(-)
---
diff --git a/gi/repo.cpp b/gi/repo.cpp
index 1d0ff369..69c1df2f 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -667,6 +667,42 @@ static JSObject* lookup_namespace(JSContext* context, JS::HandleId ns_name) {
return retval;
}
+GJS_JSAPI_RETURN_CONVENTION
+static JSObject* lookup_internal_namespace(JSContext* cx,
+ JS::HandleId ns_name) {
+ auto gcx = GjsContextPrivate::from_cx(cx);
+ JS::RootedObject global(cx, gcx->internal_global());
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+ // The internal global only supports GObject, Gio, GLib, and private
+ // namespaces.
+ if (ns_name == atoms.gobject() || ns_name == atoms.gio() ||
+ ns_name == atoms.glib() || ns_name == atoms.soup() ||
+ ns_name == atoms.private_ns_marker()) {
+ JS::RootedObject retval(cx);
+
+ if (!gjs_object_require_property(
+ cx, global, "internal namespace import", ns_name, &retval))
+ return nullptr;
+
+ return retval;
+ } else if (JSID_IS_STRING(ns_name)) {
+ JS::RootedString str(cx, JSID_TO_STRING(ns_name));
+
+ JS::UniqueChars name(gjs_string_to_utf8(cx, JS::StringValue(str)));
+
+ gjs_throw(
+ cx,
+ "Attempted to load unknown GI namespace (%s) on internal global.",
+ name.get());
+ } else {
+ gjs_throw(cx,
+ "Attempted to load invalid GI namespace on internal global.");
+ }
+
+ return nullptr;
+}
+
JSObject* gjs_lookup_namespace_object_by_name(JSContext* context,
JS::HandleId ns_name) {
JSObject* global = JS::CurrentGlobalOrNull(context);
@@ -676,6 +712,9 @@ JSObject* gjs_lookup_namespace_object_by_name(JSContext* context,
case GjsGlobalType::DEFAULT:
ns = lookup_namespace(context, ns_name);
break;
+ case GjsGlobalType::INTERNAL:
+ ns = lookup_internal_namespace(context, ns_name);
+ break;
case GjsGlobalType::DEBUGGER:
ns = nullptr;
break;
diff --git a/gjs/.eslintrc.yml b/gjs/.eslintrc.yml
new file mode 100644
index 00000000..9b4f5542
--- /dev/null
+++ b/gjs/.eslintrc.yml
@@ -0,0 +1,22 @@
+---
+extends: ../.eslintrc.yml
+globals:
+ ARGV: off
+ Debugger: readonly
+ GIRepositoryGType: off
+ globalThis: readonly
+ imports: off
+ Intl: readonly
+ log: readonly
+ logError: readonly
+ print: readonly
+ printerr: readonly
+ GLib: readonly
+ Gio: readonly
+ ByteUtils: readonly
+ lookupModule: readonly
+ registerModule: readonly
+ lookupInternalModule: readonly
+ registerInternalModule: readonly
+ setModuleResolveHook: readonly
+ getModuleURI: readonly
\ No newline at end of file
diff --git a/gjs/atoms.h b/gjs/atoms.h
index baf1e681..a996b5b2 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -71,6 +71,7 @@
macro(prototype, "prototype") \
macro(search_path, "searchPath") \
macro(signal_id, "signalId") \
+ macro(soup, "Soup") \
macro(stack, "stack") \
macro(to_string, "toString") \
macro(value_of, "valueOf") \
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 2b07f358..df7c6bb5 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -201,15 +201,15 @@ int define_argv_and_eval_script(GjsContext* js_context, int argc,
int code;
if (exec_as_module) {
GjsAutoUnref<GFile> output = g_file_new_for_commandline_arg(filename);
- char* full_path = g_file_get_path(output);
- if (!gjs_context_register_module(js_context, full_path, full_path,
- script, len, &error)) {
+ char* uri = g_file_get_uri(output);
+ if (!gjs_context_register_module(js_context, uri, uri, script, len,
+ &error)) {
g_printerr("%s\n", error->message);
code = 1;
}
uint8_t code_8 = 0;
- if (!gjs_context_eval_module(js_context, full_path, &code_8, &error)) {
+ if (!gjs_context_eval_module(js_context, uri, &code_8, &error)) {
code = code_8;
if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT))
g_critical("%s", error->message);
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 537ae2de..bd711ba5 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -95,6 +95,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;
@@ -187,6 +188,9 @@ class GjsContextPrivate : public JS::JobQueue {
GJS_USE GjsContext* public_context(void) const { return m_public_context; }
GJS_USE JSContext* context(void) const { return m_cx; }
GJS_USE JSObject* global(void) const { return m_global.get(); }
+ GJS_USE JSObject* internal_global(void) const {
+ return m_internal_global.get();
+ }
GJS_USE GjsProfiler* profiler(void) const { return m_profiler; }
GJS_USE const GjsAtoms& atoms(void) const { return *m_atoms; }
GJS_USE bool destroying(void) const { return m_destroying; }
@@ -259,7 +263,8 @@ class GjsContextPrivate : public JS::JobQueue {
void unregister_unhandled_promise_rejection(uint64_t id);
bool register_module(const char* identifier, const char* filename,
- const char* mod_text, size_t mod_len, GError** error);
+ const char* module, ssize_t module_len,
+ GError** error);
void set_sweeping(bool value);
diff --git a/gjs/context.cpp b/gjs/context.cpp
index e705a793..b3e45557 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -82,9 +82,11 @@
#include "gjs/error-types.h"
#include "gjs/global.h"
#include "gjs/importer.h"
+#include "gjs/internal.h"
#include "gjs/jsapi-util-args.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"
@@ -333,6 +335,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);
@@ -419,6 +423,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;
@@ -504,23 +509,55 @@ 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)) {
gjs_log_exception(m_cx);
g_error("Failed to initialize global strings");
}
+
+ if (!gjs_define_global_properties(m_cx, internal_global,
+ GjsGlobalType::INTERNAL,
+ "GJS internal global", "nullptr")) {
+ gjs_log_exception(m_cx);
+ g_warning("Failed to define properties on internal global object.");
+ }
+
+ JS::RootedObject global(
+ m_cx,
+ gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
+
+ if (!global) {
+ gjs_log_exception(m_cx);
+ g_error("Failed to initialize global object");
+ }
+
+ m_global = global;
+
+ // Load internal script *must* be called from the internal realm.
+
+ // TODO(ewlsh): Consider moving internal imports into JavaScript
+
+ if (!gjs_load_internal_script(cx, "errorTypes") ||
+ !gjs_load_internal_script(cx, "module") ||
+ !gjs_load_internal_script(cx, "module/loaders/file")) {
+ gjs_log_exception(cx);
+ g_error("Failed to load internal module loaders.");
+ }
+
+ auto realm = JS::EnterRealm(m_cx, global);
+
std::vector<std::string> paths;
if (m_search_path)
paths = {m_search_path, m_search_path + g_strv_length(m_search_path)};
@@ -542,6 +579,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
gjs_log_exception(m_cx);
g_error("Failed to define properties on global object");
}
+
+ JS::LeaveRealm(m_cx, realm);
}
void GjsContextPrivate::set_args(std::vector<std::string> args) {
@@ -1053,24 +1092,163 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
bool GjsContextPrivate::eval_module(const char* identifier,
uint8_t* exit_status_p, GError** error) {
- // TODO Implement eval_module
- g_error(
- "GjsContextPrivate::eval_module is not implemented. Exiting with "
- "error.");
+ bool auto_profile = m_should_profile;
- return false;
+ if (auto_profile &&
+ (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2))
+ auto_profile = false;
+
+ if (auto_profile)
+ gjs_profiler_start(m_profiler);
+
+ JSAutoRealm ac(m_cx, m_global);
+
+ auto it = gjs_get_esm_registry(m_cx)->lookup(identifier);
+
+ if (!it) {
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Cannot find module with identifier %s.", identifier);
+ return false;
+ }
+
+ bool ok = true;
+
+ JS::RootedObject obj(m_cx, it->value());
+
+ if (!JS::ModuleInstantiate(m_cx, obj)) {
+ gjs_log_exception(m_cx);
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Failed to instantiate module %s.", identifier);
+
+ return false;
+ }
+
+ if (!JS::ModuleEvaluate(m_cx, obj)) {
+ ok = false;
+ }
+
+ schedule_gc_if_needed();
+
+ if (JS_IsExceptionPending(m_cx)) {
+ gjs_log_exception(m_cx);
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Uncaught exception in %s.", identifier);
+ return false;
+ }
+
+ gjs_debug(GJS_DEBUG_CONTEXT, "Module evaluation succeeded for module %s.",
+ identifier);
+
+ /* 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;
+ }
+
+ if (auto_profile)
+ gjs_profiler_stop(m_profiler);
+
+ if (!ok) {
+ uint8_t code;
+
+ if (should_exit(&code)) {
+ *exit_status_p = code;
+ g_set_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT,
+ "Exit with code %d", code);
+ return false;
+ }
+
+ if (!JS_IsExceptionPending(m_cx)) {
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Module %s terminated with an uncatchable exception",
+ identifier);
+ } else {
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Module %s threw an exception", identifier);
+ }
+
+ gjs_log_exception(m_cx);
+ /* No exit code from script, but we don't want to exit(0) */
+ *exit_status_p = 1;
+ return false;
+ }
+
+ if (exit_status_p) {
+ /* Assume success if no integer was returned */
+ *exit_status_p = 0;
+ }
+
+ return true;
}
bool GjsContextPrivate::register_module(const char* identifier,
const char* filename,
- const char* module, size_t module_len,
+ const char* script, ssize_t script_len,
GError** error) {
- // TODO Implement register_module
- g_warning(
- "GjsContextPrivate::register_module is not yet implemented. Printing "
- "module text...");
- g_warning("%s", module);
- return true;
+ JSAutoRealm ac(m_cx, m_global);
+
+ // Module registration uses exceptions to report errors
+ // so we'll store the exception state, clear it, attempt to load the
+ // module, then restore the original exception state.
+ JS::AutoSaveExceptionState exp_state(m_cx);
+
+ auto module = new GjsESModule(identifier, filename);
+
+ auto esm_registry = gjs_get_esm_registry(m_cx);
+
+ auto it = esm_registry->lookupForAdd(identifier);
+
+ if (it.found()) {
+ gjs_throw(m_cx, "Module '%s' already registered", identifier);
+ return false;
+ }
+
+ JS::RootedObject module_record(m_cx,
+ module->compile(m_cx, script, script_len));
+
+ GjsAutoChar iden(g_strdup(identifier));
+
+ if (module_record && !esm_registry->add(it, iden.get(), module_record)) {
+ JS_ReportOutOfMemory(m_cx);
+ return false;
+ }
+
+ if (module_record) {
+ return true;
+ }
+
+ // Our message could come from memory owned by us or by the runtime.
+ const char* msg = nullptr;
+
+ JS::RootedValue exc(m_cx);
+ if (JS_GetPendingException(m_cx, &exc)) {
+ JS::RootedObject exc_obj(m_cx, &exc.toObject());
+ JSErrorReport* report = JS_ErrorFromException(m_cx, exc_obj);
+ if (report) {
+ msg = report->message().c_str();
+ } else {
+ JS::RootedString js_message(m_cx, JS::ToString(m_cx, exc));
+
+ if (js_message) {
+ JS::UniqueChars cstr(JS_EncodeStringToUTF8(m_cx, js_message));
+ msg = cstr.get();
+ }
+ }
+ }
+
+ g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+ "Error registering module '%s': %s", identifier,
+ msg ? msg : "unknown");
+
+ // We've successfully handled the exception so we can clear it.
+ // This is necessary because AutoSaveExceptionState doesn't erase
+ // exceptions when it restores the previous exception state.
+ JS_ClearPendingException(m_cx);
+
+ return false;
}
bool
diff --git a/gjs/global.cpp b/gjs/global.cpp
index ec27d2cb..7af976b5 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -58,6 +58,7 @@
#include "gjs/context-private.h"
#include "gjs/engine.h"
#include "gjs/global.h"
+#include "gjs/internal.h"
#include "gjs/jsapi-util-args.h"
#include "gjs/jsapi-util.h"
#include "gjs/module.h"
@@ -296,6 +297,9 @@ const JSClassOps defaultclassops = JS::DefaultGlobalClassOps;
class GjsGlobal {
static void finalize(JSFreeOp* op G_GNUC_UNUSED, JSObject* obj) {
+ delete static_cast<GjsModuleRegistry*>(
+ gjs_get_global_slot(obj, GjsGlobalSlot::MODULE_REGISTRY)
+ .toPrivate());
delete static_cast<GjsModuleRegistry*>(
gjs_get_global_slot(obj, GjsGlobalSlot::NATIVE_REGISTRY)
.toPrivate());
@@ -330,11 +334,24 @@ class GjsGlobal {
JS_FN("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS),
JS_FS_END};
+ static void setup_global(JSContext* cx, JSObject* global) {
+ JSAutoRealm ac(cx, global);
+
+ JSRuntime* rt = JS_GetRuntime(cx);
+
+ JS::SetModuleResolveHook(rt, gjs_module_resolve);
+ JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
+ }
+
public:
GJS_USE
static JSObject* create(JSContext* cx) {
auto global = global_create_new(cx, &klass);
+ if (global) {
+ setup_global(cx, global);
+ }
+
return global;
}
@@ -343,6 +360,10 @@ class GjsGlobal {
JSObject* cmp_global) {
auto global = global_create_with_existing(cx, cmp_global, &klass);
+ if (global) {
+ setup_global(cx, global);
+ }
+
return global;
}
@@ -350,20 +371,24 @@ class GjsGlobal {
static bool define_properties(JSContext* cx, JS::HandleObject global,
const char* realm_name,
const char* bootstrap_script) {
- const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
- if (!JS_DefinePropertyById(cx, global, atoms.window(), global,
- JSPROP_READONLY | JSPROP_PERMANENT) ||
- !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs))
- return false;
-
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);
+
+ gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+ JS::PrivateValue(new GjsModuleRegistry()));
gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
JS::PrivateValue(new GjsModuleRegistry()));
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+ if (!JS_DefinePropertyById(cx, global, atoms.window(), global,
+ JSPROP_READONLY | JSPROP_PERMANENT) ||
+ !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs))
+ return false;
+
JS::Value v_importer =
gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
g_assert(((void)"importer should be defined before passing null "
@@ -445,6 +470,131 @@ class GjsDebuggerGlobal {
}
};
+class GjsInternalGlobal {
+ static constexpr JSFunctionSpec static_funcs[] = {
+ JS_FN("getModuleURI", GetModuleURI, 1, 0),
+ JS_FN("compileAndEvalModule", CompileAndEvalModule, 1, 0),
+ JS_FN("debug", Debug, 1, 0),
+ JS_FN("lookupInternalModule", LookupInternalModule, 1, 0),
+ JS_FN("lookupModule", LookupModule, 1, 0),
+ JS_FN("registerModule", RegisterModule, 5, 0),
+ JS_FN("registerInternalModule", RegisterInternalModule, 5, 0),
+ JS_FN("setModuleResolveHook", SetModuleResolveHook, 1, 0),
+ JS_FS_END};
+
+ static void finalize(JSFreeOp* op G_GNUC_UNUSED, JSObject* obj) {
+ delete static_cast<GjsModuleRegistry*>(
+ gjs_get_global_slot(obj, GjsInternalGlobalSlot::SCRIPT_REGISTRY)
+ .toPrivate());
+ delete static_cast<GjsModuleRegistry*>(
+ gjs_get_global_slot(obj, GjsGlobalSlot::MODULE_REGISTRY)
+ .toPrivate());
+ }
+
+ static constexpr JSClassOps classops = {nullptr, // addProperty
+ nullptr, // deleteProperty
+ nullptr, // enumerate
+ JS_NewEnumerateStandardClasses,
+ JS_ResolveStandardClass,
+ JS_MayResolveStandardClass,
+ GjsInternalGlobal::finalize,
+ nullptr, // call
+ nullptr, // hasInstance
+ nullptr, // construct
+ JS_GlobalObjectTraceHook};
+
+ static constexpr JSClass klass = {
+ "GjsInternalGlobal",
+ JSCLASS_FOREGROUND_FINALIZE |
+ JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+ static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+ &classops,
+ };
+
+ public:
+ GJS_USE
+ static JSObject* create(JSContext* cx) {
+ return global_create_new(cx, &klass);
+ }
+
+ GJS_USE
+ static JSObject* create_with_compartment(JSContext* cx,
+ JSObject* cmp_global) {
+ return global_create_with_existing(cx, cmp_global, &klass);
+ }
+
+ static bool define_properties(JSContext* cx, JS::HandleObject global,
+ const char* realm_name,
+ const char* bootstrap_script G_GNUC_UNUSED) {
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+ 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);
+
+ gjs_set_global_slot(global, GjsInternalGlobalSlot::SCRIPT_REGISTRY,
+ JS::PrivateValue(new GjsModuleRegistry()));
+ gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+ JS::PrivateValue(new GjsModuleRegistry()));
+
+ if (!JS_DefineFunctions(cx, global, static_funcs)) {
+ return false;
+ }
+
+ // GI Modules
+
+ GError* error = nullptr;
+
+ if (!g_irepository_require(nullptr, "GObject", "2.0",
+ GIRepositoryLoadFlags(0), &error) ||
+ !g_irepository_require(nullptr, "GLib", "2.0",
+ GIRepositoryLoadFlags(0), &error) ||
+ !g_irepository_require(nullptr, "Gio", "2.0",
+ GIRepositoryLoadFlags(0), &error) ||
+ !g_irepository_require(nullptr, "Soup", "2.4",
+ GIRepositoryLoadFlags(0), &error)) {
+ gjs_throw_gerror_message(cx, error);
+ g_error_free(error);
+ return false;
+ }
+
+ JS::RootedObject gobject(cx, gjs_create_ns(cx, "GObject"));
+ JS::RootedObject glib(cx, gjs_create_ns(cx, "GLib"));
+ JS::RootedObject gio(cx, gjs_create_ns(cx, "Gio"));
+ JS::RootedObject soup(cx, gjs_create_ns(cx, "Soup"));
+ JS::RootedObject privateNS(cx, JS_NewPlainObject(cx));
+
+ if (!JS_DefinePropertyById(cx, global, atoms.private_ns_marker(),
+ privateNS, JSPROP_PERMANENT) ||
+ !JS_DefinePropertyById(cx, global, atoms.gobject(), glib,
+ JSPROP_PERMANENT) ||
+ !JS_DefinePropertyById(cx, global, atoms.glib(), glib,
+ JSPROP_PERMANENT) ||
+ !JS_DefinePropertyById(cx, global, atoms.gio(), gio,
+ JSPROP_PERMANENT) ||
+ !JS_DefinePropertyById(cx, global, atoms.soup(), soup,
+ JSPROP_PERMANENT)) {
+ return false;
+ }
+
+ // Native Modules
+
+ JS::RootedObject byteArray(cx, JS_NewPlainObject(cx));
+
+ if (!gjs_load_native_module(cx, "_byteArrayNative", &byteArray) ||
+ !JS_DefineProperty(cx, global, "ByteUtils", byteArray,
+ JSPROP_PERMANENT)) {
+ gjs_throw(cx, "Failed to define byteArray functions.");
+ return false;
+ }
+
+ return true;
+ }
+};
+
/**
* gjs_create_global_object:
* @cx: a #JSContext
@@ -463,6 +613,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;
}
@@ -473,6 +626,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;
}
@@ -539,6 +694,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);
}
return false;
@@ -551,6 +709,8 @@ void gjs_set_global_slot(JSObject* global, GlobalSlot slot, JS::Value value) {
}
template void gjs_set_global_slot(JSObject* global, GjsGlobalSlot slot,
JS::Value value);
+template void gjs_set_global_slot(JSObject* global, GjsInternalGlobalSlot slot,
+ JS::Value value);
template <typename GlobalSlot>
JS::Value gjs_get_global_slot(JSObject* global, GlobalSlot slot) {
@@ -558,6 +718,8 @@ JS::Value gjs_get_global_slot(JSObject* global, GlobalSlot slot) {
global, JSCLASS_GLOBAL_SLOT_COUNT + static_cast<uint32_t>(slot));
}
template JS::Value gjs_get_global_slot(JSObject* global, GjsGlobalSlot slot);
+template JS::Value gjs_get_global_slot(JSObject* global,
+ GjsInternalGlobalSlot slot);
decltype(GjsGlobal::klass) constexpr GjsGlobal::klass;
decltype(GjsGlobal::classops) constexpr GjsGlobal::classops;
@@ -566,3 +728,8 @@ decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs;
decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass;
decltype(
GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs;
+
+decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass;
+decltype(GjsInternalGlobal::classops) constexpr GjsInternalGlobal::classops;
+decltype(
+ GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index 8466b897..41bbff3b 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -41,6 +41,7 @@ enum class GjsGlobalType {
enum class GjsGlobalSlot : uint32_t {
GLOBAL_TYPE = 0,
IMPORTS,
+ MODULE_REGISTRY,
NATIVE_REGISTRY,
PROTOTYPE_gtype,
PROTOTYPE_importer,
@@ -65,6 +66,12 @@ enum class GjsGlobalSlot : uint32_t {
LAST,
};
+enum class GjsInternalGlobalSlot : uint32_t {
+ SCRIPT_REGISTRY = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+ IMPORT_HOOK,
+ LAST
+};
+
bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
GjsGlobalType gjs_global_get_type(JSObject* global);
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
new file mode 100644
index 00000000..6742bd52
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,452 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "gjs/internal.h"
+
+#include <config.h>
+#include <gio/gio.h>
+#include <girepository.h>
+#include <glib-object.h>
+#include <glib.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/Promise.h>
+#include <js/PropertyDescriptor.h>
+#include <js/RootingAPI.h>
+#include <js/SourceText.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h> // for JS_DefinePropertyById, ...
+#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/context-private.h"
+#include "gjs/context.h"
+#include "gjs/error-types.h"
+#include "gjs/global.h"
+#include "gjs/importer.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/mem-private.h"
+#include "gjs/module.h"
+#include "gjs/native.h"
+#include "util/log.h"
+
+// You have to be very careful in this file to only do operations within the
+// correct global!
+using AutoGFile = GjsAutoUnref<GFile>;
+
+bool gjs_load_internal_script(JSContext* cx, const char* identifier) {
+ GjsAutoChar full_path(g_strdup_printf(
+ "resource://org/gnome/gjs/gjs/internal/%s.js", identifier));
+ AutoGFile gfile(g_file_new_for_uri(full_path));
+
+ char* script_text_raw;
+ gsize script_text_len;
+ GError* error = nullptr;
+
+ if (!g_file_load_contents(gfile, NULL, &script_text_raw, &script_text_len,
+ nullptr, &error)) {
+ gjs_throw(cx, "Failed to read internal resource: %s \n%s",
+ full_path.get(), error->message);
+ return false;
+ }
+
+ GjsAutoChar script_text(script_text_raw);
+
+ JS::CompileOptions options(cx);
+ options.setIntroductionType("Internal Script Loader");
+ options.setFileAndLine(full_path, 1);
+ options.setSelfHostingMode(false);
+
+ std::u16string utf16_string =
+ gjs_utf8_script_to_utf16(script_text, script_text_len);
+ // 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::RootedObject internal_global(cx, gjs_get_internal_global(cx));
+
+ JSAutoRealm ar(cx, internal_global);
+
+ JS::RootedValue ignored_retval(cx);
+ JS::RootedObject module(cx, JS_NewPlainObject(cx));
+ JS::RootedObjectVector scope_chain(cx);
+
+ if (!scope_chain.append(module)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+
+ auto success = JS::Evaluate(cx, scope_chain, options, buf, &ignored_retval);
+
+ if (!success) {
+ gjs_log_exception(cx);
+ return false;
+ }
+ auto add = gjs_get_internal_script_registry(cx)->lookupForAdd(identifier);
+
+ if (add.found()) {
+ gjs_throw(cx, "Internal script %s already loaded.", identifier);
+ return false;
+ }
+
+ GjsAutoChar iden(g_strdup(identifier));
+
+ if (!gjs_get_internal_script_registry(cx)->add(add, iden.get(), module)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+ return true;
+}
+
+bool GetModuleURI(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+ if (!args.requireAtLeast(cx, "getModuleURI", 1)) {
+ return false;
+ }
+
+ JS::RootedValue importer(cx, args[0]);
+
+ if (importer.isUndefined()) {
+ gjs_throw(cx,
+ "Cannot import from relative path when module path "
+ "is unknown.");
+ return false;
+ }
+ // The module from which the resolve request is coming
+ GjsESModule* priv_module = static_cast<GjsESModule*>(importer.toPrivate());
+ // Get the module's path.
+ auto module_location = priv_module->uri();
+ // Get the module's directory.
+ const gchar* module_file_location = module_location.c_str();
+ return gjs_string_from_utf8(cx, module_file_location, args.rval());
+}
+
+bool SetModuleResolveHook(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+ if (!args.requireAtLeast(cx, "setModuleResolveHook", 1)) {
+ return false;
+ }
+
+ JS::RootedValue mv(cx, args[0]);
+
+ // The hook is stored in the internal global.
+ JS::RootedObject global(cx, gjs_get_internal_global(cx));
+ gjs_set_global_slot(global, GjsInternalGlobalSlot::IMPORT_HOOK, mv);
+
+ args.rval().setUndefined();
+ return true;
+}
+
+bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "compileAndEvalModule", 1)) {
+ return false;
+ }
+
+ JS::RootedString s1(cx, args[0].toString());
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+ {
+ JSAutoRealm ar(cx, gjs_get_import_global(cx));
+ auto registry = gjs_get_esm_registry(cx);
+ auto result = registry->lookup(id.get());
+ if (result) {
+ JS::RootedObject res(cx, result->value());
+ auto init = JS::ModuleInstantiate(cx, res);
+ if (!init) {
+ gjs_log_exception(cx);
+ }
+
+ auto eval = JS::ModuleEvaluate(cx, res);
+
+ if (!eval) {
+ gjs_log_exception(cx);
+ }
+
+ args.rval().setBoolean(init && eval);
+ } else {
+ args.rval().setBoolean(false);
+ }
+ }
+
+ return true;
+}
+
+bool gjs_require_module(JSContext* m_cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+ if (argc != 1) {
+ gjs_throw(m_cx, "Must pass a single argument to require()");
+ return false;
+ }
+
+ JS::AutoSaveExceptionState exc_state(m_cx);
+ JS::RootedString jstr(m_cx, JS::ToString(m_cx, argv[0]));
+ exc_state.restore();
+
+ if (!jstr) {
+ g_message("JS LOG: <cannot convert value to string>");
+ return true;
+ }
+
+ JS::UniqueChars id(JS_EncodeStringToUTF8(m_cx, jstr));
+
+ if (!id) {
+ gjs_throw(m_cx, "Invalid native id.");
+ return false;
+ }
+
+ auto native_registry = gjs_get_native_module_registry(m_cx);
+
+ auto nativeModuleAdd = native_registry->lookupForAdd(id.get());
+
+ if (nativeModuleAdd.found()) {
+ JS::RootedObject obj(m_cx, nativeModuleAdd->value().get());
+
+ argv.rval().setObject(*obj);
+ return true;
+ }
+
+ JS::RootedObject native_obj(m_cx);
+
+ if (!gjs_load_native_module(m_cx, id.get(), &native_obj)) {
+ gjs_throw(m_cx, "Failed to load native module: %s", id.get());
+ return false;
+ }
+
+ if (!native_registry->add(nativeModuleAdd, id.get(), native_obj)) {
+ JS_ReportOutOfMemory(m_cx);
+ return false;
+ }
+
+ argv.rval().setObject(*native_obj);
+ return true;
+}
+
+static bool register_module(JSContext* cx, const char* identifier,
+ const char* path, const char* text, size_t length,
+ bool* success) {
+ auto esm_registry = gjs_get_esm_registry(cx);
+
+ auto it = esm_registry->lookupForAdd(path);
+
+ if (it.found()) {
+ gjs_throw(cx, "Module '%s' already registered", path);
+ return false;
+ }
+
+ auto module = new GjsESModule(identifier, path);
+
+ JS::RootedObject module_record(cx, module->compile(cx, text, length));
+
+ if (module_record && !esm_registry->add(it, identifier, module_record)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+
+ *success = module_record;
+
+ return true;
+}
+
+// registerModule(id: string, path: string, text: string, length: number,
+// unused: boolean)
+bool RegisterModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "registerModule", 5)) {
+ return false;
+ }
+
+ JS::RootedString str0(cx, args[0].toString()), // id
+ str1(cx, args[1].toString()), // path
+ str2(cx, args[2].toString()); // text
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, str0);
+ JS::UniqueChars path = JS_EncodeStringToUTF8(cx, str1);
+ JS::UniqueChars text = JS_EncodeStringToUTF8(cx, str2);
+ auto length = args[3].toInt32();
+
+ {
+ JSAutoRealm ar(cx, gjs_get_import_global(cx));
+
+ bool success = false;
+ bool result = register_module(cx, id.get(), path.get(), text.get(),
+ length, &success);
+
+ args.rval().setBoolean(success);
+
+ return result;
+ }
+}
+
+static bool register_internal_module(JSContext* cx, const char* identifier,
+ const char* filename, const char* module,
+ size_t module_len, bool* success) {
+ auto internal_registry = gjs_get_internal_module_registry(cx);
+ auto it = internal_registry->lookupForAdd(identifier);
+
+ if (it.found()) {
+ gjs_throw(cx, "Internal module '%s' is already registered", identifier);
+ return false;
+ }
+
+ auto internal_module = new GjsESModule(identifier, filename, true);
+
+ JS::RootedObject module_record(
+ cx, internal_module->compile(cx, module, module_len));
+
+ GjsAutoChar iden(g_strdup(identifier));
+
+ if (module_record && !internal_registry->add(it, iden, module_record)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+
+ *success = module_record;
+
+ return true;
+}
+
+// registerInternalModule(id: string, path: string, text: string, length:
+// number)
+bool RegisterInternalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "registerInternalModule", 4)) {
+ return false;
+ }
+
+ JS::RootedString str0(cx, args[0].toString()), // id
+ str1(cx, args[1].toString()), // path
+ str2(cx, args[2].toString()); // text
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, str0);
+ JS::UniqueChars path = JS_EncodeStringToUTF8(cx, str1);
+ JS::UniqueChars text = JS_EncodeStringToUTF8(cx, str2);
+ auto length = args[3].toInt32();
+
+ {
+ JSAutoRealm ar(cx, gjs_get_import_global(cx));
+
+ bool success = false;
+ bool result = register_internal_module(cx, id.get(), path.get(),
+ text.get(), length, &success);
+ args.rval().setBoolean(success);
+
+ return result;
+ }
+}
+
+// lookupInternalModule(id: string)
+bool LookupInternalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "lookupInternalModule", 1)) {
+ return false;
+ }
+
+ JS::RootedString s1(cx, args[0].toString());
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+ {
+ JSAutoRealm ar(cx, gjs_get_import_global(cx));
+ auto registry = gjs_get_internal_module_registry(cx);
+ auto it = registry->lookup(id.get());
+
+ if (!it.found()) {
+ args.rval().setNull();
+ return true;
+ }
+ JS::RootedObject lookup(cx, it->value());
+ if (!lookup) {
+ args.rval().setNull();
+ } else {
+ args.rval().setObject(*lookup.get());
+ }
+ }
+ return true;
+}
+
+// lookupModule(id: string)
+bool LookupModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "lookupModule", 1)) {
+ return false;
+ }
+
+ JS::RootedString s1(cx, args[0].toString());
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+ {
+ JSAutoRealm ar(cx, gjs_get_import_global(cx));
+ auto registry = gjs_get_esm_registry(cx);
+ auto it = registry->lookup(id.get());
+
+ if (!it.found()) {
+ args.rval().setNull();
+ return true;
+ }
+ JS::RootedObject lookup(cx, it->value());
+
+ if (!lookup) {
+ args.rval().setNull();
+ } else {
+ args.rval().setObject(*lookup.get());
+ }
+ }
+ return true;
+}
+
+// debug(msg: string)
+bool Debug(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.requireAtLeast(cx, "debug", 1)) {
+ return false;
+ }
+
+ JS::RootedString s1(cx, args[0].toString());
+
+ JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+ gjs_debug(GJS_DEBUG_IMPORTER, id.get());
+
+ return true;
+}
diff --git a/gjs/internal.h b/gjs/internal.h
new file mode 100644
index 00000000..475b6ce2
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef GJS_INTERNAL_H_
+#define GJS_INTERNAL_H_
+
+#include <config.h>
+#include <gio/gio.h>
+#include <js/GCHashTable.h>
+#include <js/GCVector.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h> // for JS_GetContextPrivate
+#include <jsfriendapi.h> // for ScriptEnvironmentPreparer
+
+#include <string>
+
+#include "gjs/macros.h"
+
+bool gjs_load_internal_script(JSContext* cx, const char* identifier);
+
+// setModuleResolveHook
+bool SetModuleResolveHook(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// compileAndEvalModule(id: string)
+bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// registerModule(id: string, path: string, text: string, length: number, ?:
+// boolean)
+bool RegisterModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// registerInternalModule(id: string, path: string, text: string, length:
+// number, is_legacy: boolean)
+bool RegisterInternalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// lookupInternalModule(id: string)
+bool LookupInternalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// lookupModule(id: string)
+bool LookupModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// debug(msg: string)
+bool Debug(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// getModuleURI(module): string
+bool GetModuleURI(JSContext* cx, unsigned argc, JS::Value* vp);
+
+#endif // GJS_INTERNAL_H_
diff --git a/gjs/internal/errorTypes.js b/gjs/internal/errorTypes.js
new file mode 100644
index 00000000..94a6c355
--- /dev/null
+++ b/gjs/internal/errorTypes.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+class ImportError extends Error {
+ constructor(message) {
+ super(message);
+
+ this.name = 'ImportError';
+ }
+}
+
+globalThis.ImportError = ImportError;
diff --git a/gjs/internal/module.js b/gjs/internal/module.js
new file mode 100644
index 00000000..37539bc9
--- /dev/null
+++ b/gjs/internal/module.js
@@ -0,0 +1,259 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* global debug, ImportError */
+
+if (typeof ImportError !== 'function') {
+ throw new Error('ImportError is not defined in module loader.');
+}
+
+// NOTE: Gio, GLib, and GObject have no overrides.
+
+function isRelativePath(id) {
+ // Check if the path is relative.
+ return id.startsWith('./') || id.startsWith('../');
+}
+
+const allowedRelatives = ["file", "resource"];
+
+const relativeResolvers = new Map();
+const loaders = new Map();
+
+function registerScheme(...schemes) {
+ function forEach(fn, ...args) {
+ schemes.forEach(s => fn(s, ...args));
+ }
+
+ const schemeBuilder = {
+ relativeResolver(handler) {
+ forEach((scheme) => {
+ allowedRelatives.push(scheme);
+ relativeResolvers.set(scheme, handler);
+ });
+
+ return schemeBuilder;
+ },
+ loader(handler) {
+ forEach(scheme => {
+ loaders.set(scheme, handler);
+ });
+
+ return schemeBuilder;
+ }
+ };
+
+ return Object.freeze(schemeBuilder);
+}
+
+globalThis.registerScheme = registerScheme;
+
+function parseURI(uri) {
+ const parsed = GLib.uri_parse_scheme(uri);
+
+ if (!parsed) {
+ return null;
+ }
+
+ return {
+ raw: uri,
+ scheme: parsed,
+ };
+
+}
+
+/**
+ * @type {Set<string>}
+ *
+ * The set of "module" URIs (the module search path)
+ */
+const moduleURIs = new Set();
+
+function registerModuleURI(uri) {
+ moduleURIs.add(uri);
+}
+
+// Always let ESM-specific modules take priority over core modules.
+registerModuleURI('resource:///org/gnome/gjs/modules/esm/');
+registerModuleURI('resource:///org/gnome/gjs/modules/core/');
+
+/**
+ * @param {string} specifier
+ */
+function buildInternalURIs(specifier) {
+ const builtURIs = [];
+
+ for (const uri of moduleURIs) {
+ const builtURI = `${uri}/${specifier}.js`;
+
+ debug(`Built internal URI ${builtURI} with ${specifier} for ${uri}.`);
+
+ builtURIs.push(builtURI);
+ }
+
+ return builtURIs
+}
+
+function resolveRelativePath(moduleURI, relativePath) {
+ // If a module has a path, we'll have stored it in the host field
+ if (!moduleURI) {
+ throw new ImportError('Cannot import from relative path when module path is unknown.');
+ }
+
+ debug(`moduleURI: ${moduleURI}`);
+
+ const parsed = parseURI(moduleURI);
+
+ // Handle relative imports from URI-based modules.
+ if (parsed) {
+ const resolver = relativeResolvers.get(parsed.scheme);
+
+ if (resolver) {
+ return resolver(parsed, relativePath);
+ } else {
+ throw new ImportError(
+ `Relative imports can only occur from the following URI schemes: ${
+ Array.from(relativeResolvers.keys()).map(s => `${s}://`).join(', ')
+ }`);
+ }
+ } else {
+ throw new ImportError(`Module has invalid URI: ${moduleURI}`);
+ }
+}
+
+function loadURI(uri) {
+ debug(`URI: ${uri.raw}`);
+
+ if (uri.scheme) {
+ const loader = loaders.get(uri.scheme);
+
+ if (loader) {
+ return loader(uri);
+ } else {
+ throw new ImportError(`No resolver found for URI: ${uri.raw || uri}`);
+ }
+ } else {
+ throw new ImportError(`Unable to load module, module has invalid URI: ${uri.raw || uri}`);
+ }
+}
+
+function resolveSpecifier(specifier, moduleURI = null) {
+ // If a module has a path, we'll have stored it in the host field
+ let output = null;
+ let uri = null;
+ let parsedURI = null;
+
+ if (isRelativePath(specifier)) {
+ let resolved = resolveRelativePath(moduleURI, specifier);
+
+ parsedURI = parseURI(resolved);
+ uri = resolved;
+ } else {
+ const parsed = parseURI(specifier);
+
+ if (parsed) {
+ uri = parsed.raw;
+ parsedURI = parsed;
+ }
+ }
+
+ if (parsedURI) {
+ output = loadURI(parsedURI);
+ }
+
+ if (!output)
+ return null;
+
+ return { output, uri };
+}
+
+function resolveModule(specifier, moduleURI) {
+ // Check if the module has already been loaded
+ //
+ // Order:
+ // - Local imports
+ // - Internal imports
+
+ debug(`Resolving: ${specifier}`);
+
+ let lookup_module = lookupModule(specifier);
+
+ if (lookup_module)
+ return lookup_module;
+
+ lookup_module = lookupInternalModule(specifier);
+
+ if (lookup_module)
+ return lookup_module;
+
+ // 1) Resolve path and URI-based imports.
+
+ const resolved = resolveSpecifier(specifier, moduleURI);
+
+ if (resolved) {
+ const { output, uri } = resolved;
+
+ debug(`Full path found: ${uri}`);
+
+ lookup_module = lookupModule(uri);
+
+ // Check if module is already loaded (relative handling)
+ if (lookup_module)
+ return lookup_module;
+
+ const text = output;
+
+ if (!registerModule(uri, uri, text, text.length, false))
+ throw new ImportError(`Failed to register module: ${uri}`);
+
+
+ return lookupModule(uri);
+ }
+
+ // 2) Resolve internal imports.
+
+ const uri = buildInternalURIs(specifier).find((uri) => {
+ let file = Gio.File.new_for_uri(uri);
+
+ return file && file.query_exists(null);
+ });
+
+ if (!uri)
+ throw new ImportError(`Attempted to load unregistered global module: ${specifier}`);
+
+ const text = loaders.get('resource')(parseURI(uri));
+
+ if (!registerInternalModule(specifier, uri, text, text.length))
+ return null;
+
+ return lookupInternalModule(specifier);
+}
+
+setModuleResolveHook((referencingInfo, specifier) => {
+ debug('Starting module import...');
+ const uri = getModuleURI(referencingInfo);
+
+ if (uri) {
+ debug(`Found base URI: ${uri}`);
+ }
+
+ return resolveModule(specifier, uri);
+});
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 4d7193b2..e997bdb7 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -657,6 +657,10 @@ JSObject* gjs_get_import_global(JSContext* cx) {
return GjsContextPrivate::from_cx(cx)->global();
}
+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 a4bee3bc..91bf08da 100644
--- a/gjs/jsapi-util.h
+++ b/gjs/jsapi-util.h
@@ -212,6 +212,9 @@ struct GCPolicy<GjsAutoParam> : public IgnoreGCPolicy<GjsAutoParam> {};
GJS_USE
JSObject* gjs_get_import_global (JSContext *context);
+GJS_USE
+JSObject* gjs_get_internal_global(JSContext* context);
+
void gjs_throw_constructor_error (JSContext *context);
void gjs_throw_abstract_constructor_error(JSContext *context,
diff --git a/gjs/module.cpp b/gjs/module.cpp
index c0ccdc36..564f013b 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -252,6 +252,30 @@ class GjsScriptModule {
}
};
+JSObject* GjsESModule::compile(JSContext* m_cx, const char* mod_text,
+ size_t mod_len) {
+ JS::CompileOptions options(m_cx);
+ options.setFileAndLine(m_uri.c_str(), 1).setSourceIsLazy(false);
+
+ std::u16string utf16_string(gjs_utf8_script_to_utf16(mod_text, mod_len));
+
+ JS::SourceText<char16_t> buf;
+ if (!buf.init(m_cx, utf16_string.c_str(), utf16_string.size(),
+ JS::SourceOwnership::Borrowed))
+ return nullptr;
+
+ JS::RootedObject new_module(m_cx);
+
+ if (!JS::CompileModule(m_cx, options, buf, &new_module)) {
+ gjs_log_exception(m_cx);
+ return nullptr;
+ }
+
+ JS::SetModulePrivate(new_module, JS::PrivateValue(this));
+
+ return new_module;
+}
+
/**
* gjs_module_import:
* @cx: the JS context
@@ -288,3 +312,106 @@ GjsModuleRegistry* gjs_get_native_module_registry(JSContext* js_context) {
return static_cast<GjsModuleRegistry*>(native_registry.toPrivate());
}
+
+GjsModuleRegistry* gjs_get_esm_registry(JSContext* js_context) {
+ auto global = gjs_get_import_global(js_context);
+ auto esm_registry =
+ gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
+
+ return static_cast<GjsModuleRegistry*>(esm_registry.toPrivate());
+}
+
+GjsModuleRegistry* gjs_get_internal_script_registry(JSContext* js_context) {
+ auto global = gjs_get_internal_global(js_context);
+ auto script_registry =
+ gjs_get_global_slot(global, GjsInternalGlobalSlot::SCRIPT_REGISTRY);
+
+ return static_cast<GjsModuleRegistry*>(script_registry.toPrivate());
+}
+
+GjsModuleRegistry* gjs_get_internal_module_registry(JSContext* js_context) {
+ auto global = gjs_get_internal_global(js_context);
+ auto script_registry =
+ gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
+
+ return static_cast<GjsModuleRegistry*>(script_registry.toPrivate());
+}
+
+static bool populate_module_meta(JSContext* m_cx,
+ JS::Handle<JS::Value> private_ref,
+ JS::Handle<JSObject*> meta_object_handle) {
+ JS::RootedObject meta_object(m_cx, meta_object_handle);
+ JS::RootedValue uri_val(m_cx, JS::UndefinedValue());
+ bool allow_require = true;
+
+ if (!private_ref.isUndefined()) {
+ GjsESModule* module =
+ static_cast<GjsESModule*>(private_ref.toPrivate());
+
+ auto uri = module->uri();
+
+ allow_require = module->isInternal();
+
+ JS::Rooted<JSString*> uri_str(m_cx,
+ JS_NewStringCopyZ(m_cx, uri.c_str()));
+
+ if (!uri_str) {
+ JS_ReportOutOfMemory(m_cx);
+ return false;
+ }
+
+ uri_val.setString(uri_str);
+ }
+
+ if (!JS_DefineProperty(m_cx, meta_object, "url", uri_val,
+ JSPROP_ENUMERATE)) {
+ gjs_throw(m_cx, "Could not define import.meta.url");
+ return false;
+ }
+
+ if (allow_require) {
+ if (!JS_DefineFunction(m_cx, meta_object, "require", gjs_require_module,
+ 1, GJS_MODULE_PROP_FLAGS)) {
+ gjs_throw(m_cx, "Could not define require!");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importer,
+ JS::HandleString specifier) {
+ g_assert(gjs_global_is_type(cx, GjsGlobalType::DEFAULT) &&
+ "gjs_module_resolve can only be called from on the default "
+ "global.");
+
+ GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+
+ JS::RootedObject global(cx, gjs_cx->internal_global());
+ JSAutoRealm ar(cx, global);
+
+ JS::RootedValue hookValue(
+ cx, gjs_get_global_slot(global, GjsInternalGlobalSlot::IMPORT_HOOK));
+
+ JS::AutoValueArray<3> args(cx);
+ args[0].set(importer);
+ args[1].setString(specifier);
+
+ JS::RootedValue result(cx);
+
+ if (!JS_CallFunctionValue(cx, nullptr, hookValue, args, &result)) {
+ gjs_log_exception(cx);
+ return nullptr;
+ }
+
+ JS::RootedObject module(cx, result.toObjectOrNull());
+
+ return module;
+}
+
+bool gjs_populate_module_meta(JSContext* m_cx,
+ JS::Handle<JS::Value> private_ref,
+ JS::Handle<JSObject*> meta_object) {
+ return populate_module_meta(m_cx, private_ref, meta_object);
+}
diff --git a/gjs/module.h b/gjs/module.h
index d79205fe..a61bc470 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -73,7 +73,47 @@ gjs_module_import(JSContext *cx,
const char *name,
GFile *file);
-GJS_JSAPI_RETURN_CONVENTION
+class GjsESModule {
+ std::string m_identifier;
+ std::string m_uri;
+ bool m_is_internal;
+
+ public:
+ GjsESModule(std::string module_identifier, std::string module_uri,
+ bool is_internal) {
+ m_is_internal = is_internal;
+ m_uri = module_uri;
+ m_identifier = module_identifier;
+ }
+
+ GjsESModule(std::string module_identifier, std::string module_uri)
+ : GjsESModule(module_identifier, module_uri, false) {}
+
+ void setUri(std::string uri) { m_uri = uri; }
+
+ std::string uri() { return m_uri; }
+
+ std::string identifier() { return m_identifier; }
+
+ bool isInternal() { return m_is_internal; }
+
+ GJS_JSAPI_RETURN_CONVENTION
+ JSObject* compile(JSContext* cx, const char* mod_text, size_t mod_len);
+};
+
+bool gjs_require_module(JSContext* js_context, unsigned argc, JS::Value* vp);
+
GjsModuleRegistry* gjs_get_native_module_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_esm_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_internal_module_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_internal_script_registry(JSContext* js_context);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
+ JS::HandleString specifier);
+
+bool gjs_populate_module_meta(JSContext* m_cx,
+ JS::Handle<JS::Value> private_ref,
+ JS::Handle<JSObject*> meta_object);
#endif // GJS_MODULE_H_
diff --git a/js.gresource.xml b/js.gresource.xml
index f0c3b934..10653e86 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -1,6 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/gjs">
+ <!-- Internal scripts -->
+ <file>gjs/internal/errorTypes.js</file>
+ <file>gjs/internal/module.js</file>
+ <file>gjs/internal/module/loaders/file.js</file>
+
+ <!-- ESM-based modules -->
+ <file>modules/esm/gi.js</file>
+ <file>modules/esm/system.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 574ae1b1..3b72adc4 100644
--- a/meson.build
+++ b/meson.build
@@ -380,6 +380,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..610e5085
--- /dev/null
+++ b/modules/esm/.eslintrc.yml
@@ -0,0 +1,4 @@
+---
+extends: '../../.eslintrc.yml'
+parserOptions:
+ ecmaVersion: 2020
diff --git a/modules/esm/gi.js b/modules/esm/gi.js
new file mode 100644
index 00000000..274d3827
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,14 @@
+const gi = import.meta.require('gi');
+
+const Gi = {
+ require(name, version = null) {
+ if (version !== null) {
+ gi.versions[name] = version;
+ }
+
+ return gi[name];
+ }
+}
+Object.freeze(Gi);
+
+export default Gi;
\ No newline at end of file
diff --git a/modules/esm/system.js b/modules/esm/system.js
new file mode 100644
index 00000000..1523b7be
--- /dev/null
+++ b/modules/esm/system.js
@@ -0,0 +1 @@
+export default import.meta.require('system');
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]