[gjs/esm-local-imports: 5/6] Cleanup internal scripts, add module system.



commit 0a80c0a0b53c948a0a3d46b47e59386dae868b34
Author: Evan Welsh <noreply evanwelsh com>
Date:   Sun Jun 14 12:28:56 2020 -0500

    Cleanup internal scripts, add module system.

 gi/repo.cpp                         |   3 +-
 gjs/.eslintrc.yml                   |   2 +-
 gjs/atoms.h                         |   1 +
 gjs/console.cpp                     |   8 +-
 gjs/context.cpp                     |   9 +-
 gjs/global.cpp                      |   7 +-
 gjs/internal.cpp                    |   9 +-
 gjs/internal.h                      |   4 +-
 gjs/internal/errorTypes.js          |  31 +++++
 gjs/internal/module.js              | 265 ++++++++++++++++++++++++++++++++++++
 gjs/internal/module/loaders/file.js |  26 ++++
 gjs/module.js                       | 218 -----------------------------
 js.gresource.xml                    |   6 +-
 13 files changed, 354 insertions(+), 235 deletions(-)
---
diff --git a/gi/repo.cpp b/gi/repo.cpp
index ca306366..69c1df2f 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -677,7 +677,8 @@ static JSObject* lookup_internal_namespace(JSContext* 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.private_ns_marker()) {
+        ns_name == atoms.glib() || ns_name == atoms.soup() ||
+        ns_name == atoms.private_ns_marker()) {
         JS::RootedObject retval(cx);
 
         if (!gjs_object_require_property(
diff --git a/gjs/.eslintrc.yml b/gjs/.eslintrc.yml
index 748035e5..9b4f5542 100644
--- a/gjs/.eslintrc.yml
+++ b/gjs/.eslintrc.yml
@@ -19,4 +19,4 @@ globals:
   lookupInternalModule: readonly
   registerInternalModule: readonly
   setModuleResolveHook: readonly
-  getModuleUri: readonly
\ No newline at end of file
+  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 428fe1c4..8a80a93a 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -202,15 +202,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.cpp b/gjs/context.cpp
index a4d14d71..109bbb38 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -546,9 +546,14 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     m_global = global;
 
     // Load internal script *must* be called from the internal realm.
-    if (!gjs_load_internal_script(cx, "module")) {
+
+    // 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_warning("Failed to load internal dynamic module hooks.");
+        g_error("Failed to load internal module loaders.");
     }
 
     auto realm = JS::EnterRealm(m_cx, global);
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 88cb420c..381deef4 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -448,7 +448,7 @@ class GjsDebuggerGlobal {
 
 class GjsInternalGlobal {
     static constexpr JSFunctionSpec static_funcs[] = {
-        JS_FN("getModuleUri", GetModuleUri, 1, 0),
+        JS_FN("getModuleURI", GetModuleURI, 1, 0),
         JS_FN("compileAndEvalModule", CompileAndEvalModule, 1, 0),
         JS_FN("debug", Debug, 1, 0),
         JS_FN("lookupInternalModule", LookupInternalModule, 1, 0),
@@ -505,6 +505,8 @@ class GjsInternalGlobal {
             !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);
@@ -514,6 +516,7 @@ class GjsInternalGlobal {
         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(),
@@ -523,6 +526,8 @@ class GjsInternalGlobal {
             !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;
         }
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index e5c7ed7c..ffd0abe5 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -62,8 +62,8 @@
 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/%s.js", 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;
@@ -97,6 +97,7 @@ bool gjs_load_internal_script(JSContext* cx, const char* identifier) {
     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);
@@ -128,9 +129,9 @@ bool gjs_load_internal_script(JSContext* cx, const char* identifier) {
     return true;
 }
 
-bool GetModuleUri(JSContext* cx, unsigned argc, JS::Value* vp) {
+bool GetModuleURI(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = CallArgsFromVp(argc, vp);
-    if (!args.requireAtLeast(cx, "getModuleUri", 1)) {
+    if (!args.requireAtLeast(cx, "getModuleURI", 1)) {
         return false;
     }
 
diff --git a/gjs/internal.h b/gjs/internal.h
index 98aa7f8b..475b6ce2 100644
--- a/gjs/internal.h
+++ b/gjs/internal.h
@@ -60,7 +60,7 @@ 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);
+// 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..3953cd99
--- /dev/null
+++ b/gjs/internal/module.js
@@ -0,0 +1,265 @@
+/*
+ * 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, Soup, 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 = Soup.URI.new(uri);
+
+    if (!parsed) {
+        return null;
+    }
+
+    return {
+        raw: uri,
+        query: parsed.query ? Soup.form_decode(parsed.query) : {},
+        rawQuery: parsed.query,
+        scheme: parsed.scheme,
+        host: parsed.host,
+        port: parsed.port,
+        path: parsed.path,
+        fragment: parsed.fragment
+    };
+
+}
+
+/** 
+ * @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/internal/module/loaders/file.js b/gjs/internal/module/loaders/file.js
new file mode 100644
index 00000000..fc0f4f2f
--- /dev/null
+++ b/gjs/internal/module/loaders/file.js
@@ -0,0 +1,26 @@
+function fromBytes(bytes) {
+    return ByteUtils.toString(bytes, 'utf-8');
+}
+
+function loadFileSync(output, full_path) {
+    try {
+        const [, bytes] = output.load_contents(null);
+        return fromBytes(bytes);
+    } catch (error) {
+        throw new Error(`Unable to load file from: ${full_path}`);
+    }
+}
+
+registerScheme("file", "resource")
+    .relativeResolver((moduleURI, relativePath) => {
+        let module_file = Gio.File.new_for_uri(moduleURI.raw);
+        let module_parent_file = module_file.get_parent();
+
+        let output = module_parent_file.resolve_relative_path(relativePath);
+
+        return output.get_uri();
+    }).loader(uri => {
+        const file = Gio.File.new_for_uri(uri.raw);
+
+        return loadFileSync(file, file.get_uri());
+    });
diff --git a/js.gresource.xml b/js.gresource.xml
index dd5a5341..10653e86 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -2,8 +2,10 @@
 <gresources>
   <gresource prefix="/org/gnome/gjs">
     <!-- Internal scripts -->
-    <file>gjs/module.js</file>
-    
+    <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>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]