Explorar o código

Merge pull request #442 from HeyPuter/eric/extensibility

class registry for service scripts
Eric Dubé hai 1 ano
pai
achega
0672562c8a

+ 4 - 0
eslint.config.js

@@ -58,6 +58,10 @@ export default [
                 "html_encode": true,
                 "html_decode": true,
                 "isMobile": true,
+                // Class Registry
+                "logger": true,
+                "def": true,
+                "use": true,
                 // Libraries
                 "saveAs": true,         // FileSaver
                 "iro": true,            // iro.js color picker

+ 34 - 3
packages/backend/src/services/PuterHomepageService.js

@@ -114,6 +114,12 @@ class PuterHomepageService extends BaseService {
 
         const bundled = env != 'dev' || use_bundled_gui;
 
+        const writeScriptTag = path =>
+            `<script type="${
+                Array.isArray(path) ? 'text/javascirpt' : 'module'
+            }" src="${Array.isArray(path) ? path[0] : path}"></script>\n`
+            ;
+
         return `<!DOCTYPE html>
     <html lang="en">
 
@@ -161,6 +167,28 @@ class PuterHomepageService extends BaseService {
         <!-- Preload images when applicable -->
         <link rel="preload" as="image" href="${asset_dir}/images/wallpaper.webp">
 
+        <script>
+            if ( ! window.service_script ) {
+                window.service_script_api_promise = (() => {
+                    let resolve, reject;
+                    const promise = new Promise((res, rej) => {
+                        resolve = res;
+                        reject = rej;
+                    });
+                    promise.resolve = resolve;
+                    promise.reject = reject;
+                    return promise;
+                })();
+                window.service_script = async fn => {
+                    try {
+                        await fn(await window.service_script_api_promise);
+                    } catch (e) {
+                        console.error('service_script(ERROR)', e);
+                    }
+                };
+            }
+        </script>
+
         <!-- Files from JSON (may be empty) -->
         ${
             ((!bundled && manifest?.css_paths)
@@ -209,13 +237,16 @@ class PuterHomepageService extends BaseService {
 
         ${
             ((!bundled && manifest?.js_paths)
-                ? manifest.js_paths.map(path => `<script type="module" src="${path}"></script>\n`)
+                ? manifest.js_paths.map(path => writeScriptTag(path))
                 : []).join('')
         }
         <!-- Load the GUI script -->
-        <script ${ !bundled ? ' type="module"' : ''} src="${(!bundled && manifest?.index) || '/dist/gui.js'}"></script>
+        <script ${
+            // !bundled ? ' type="module"' : ''
+            ' type="module"'
+        } src="${(!bundled && manifest?.index) || '/dist/gui.js'}"></script>
         <!-- Initialize GUI when document is loaded -->
-        <script>
+        <script type="module">
         window.addEventListener('load', function() {
             gui(${
                 // TODO: override JSON.stringify to ALWAYS to this...

+ 2 - 1
puter-gui.json

@@ -24,7 +24,8 @@
             "/css/theme.css"
         ],
         "js_paths": [
-            "/src/initgui.js",
+            "/src/init_sync.js",
+            "/src/init_async.js",
             "/src/helpers.js",
             "/src/IPC.js",
             "/src/globals.js",

+ 5 - 5
src/UI/Components/Button.js

@@ -1,6 +1,8 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
+
+export default def(class Button extends Component {
+    static ID = 'ui.component.Button';
 
-export default class Button extends Component {
     static PROPERTIES = {
         label: { value: 'Test Label' },
         on_click: { value: null },
@@ -63,6 +65,4 @@ export default class Button extends Component {
             $(this.dom_).find('button').prop('disabled', ! enabled);
         });
     }
-}
-
-defineComponent('c-button', Button);
+});

+ 5 - 5
src/UI/Components/CodeEntryView.js

@@ -1,6 +1,8 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
+
+export default def(class CodeEntryView extends Component {
+    static ID = 'ui.component.CodeEntryView';
 
-export default class CodeEntryView extends Component {
     static PROPERTIES = {
         value: {},
         error: {},
@@ -213,6 +215,4 @@ export default class CodeEntryView extends Component {
             }
         });
     }
-}
-
-defineComponent('c-code-entry-view', CodeEntryView);
+})

+ 5 - 5
src/UI/Components/ConfirmationsView.js

@@ -1,9 +1,11 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * Display a list of checkboxes for the user to confirm.
  */
-export default class ConfirmationsView extends Component {
+export default def(class ConfirmationsView extends Component {
+    static ID = 'ui.component.ConfirmationsView';
+
     static PROPERTIES = {
         confirmations: {
             description: 'The list of confirmations to display',
@@ -56,6 +58,4 @@ export default class ConfirmationsView extends Component {
             }
         });
     }
-}
-
-defineComponent('c-confirmations-view', ConfirmationsView);
+});

+ 5 - 5
src/UI/Components/Flexer.js

@@ -1,10 +1,12 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * Allows a flex layout of composed components to be
  * treated as a component.
  */
-export default class Flexer extends Component {
+export default def(class Flexer extends Component {
+    static ID = 'ui.component.Flexer';
+
     static PROPERTIES = {
         children: {},
         gap: { value: '20pt' },
@@ -36,6 +38,4 @@ export default class Flexer extends Component {
             $(this.dom_).find('div').first().css('gap', gap);
         });
     }
-}
-
-defineComponent('c-flexer', Flexer);
+});

+ 5 - 5
src/UI/Components/JustHTML.js

@@ -1,9 +1,11 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * Allows using an HTML string as a component.
  */
-export default class JustHTML extends Component {
+export default def(class JustHTML extends Component {
+    static ID = 'ui.component.JustHTML';
+
     static PROPERTIES = { html: { value: '' } };
     create_template ({ template }) {
         $(template).html(`<span></span>`);
@@ -13,6 +15,4 @@ export default class JustHTML extends Component {
             $(this.dom_).find('span').html(html);
         });
     }
-}
-
-defineComponent('c-just-html', JustHTML);
+});

+ 5 - 5
src/UI/Components/PasswordEntry.js

@@ -1,6 +1,8 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
+
+export default def(class PasswordEntry extends Component {
+    static ID = 'ui.component.PasswordEntry';
 
-export default class PasswordEntry extends Component {
     static PROPERTIES = {
         spec: {},
         value: {},
@@ -131,6 +133,4 @@ export default class PasswordEntry extends Component {
             $(this.dom_).find("#toggle-show-password").attr("src", show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"])
         });
     }
-}
-
-defineComponent('c-password-entry', PasswordEntry);
+});

+ 5 - 5
src/UI/Components/QRCode.js

@@ -1,7 +1,9 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 import UIComponentWindow from "../UIComponentWindow.js";
 
-export default class QRCodeView extends Component {
+export default def(class QRCodeView extends Component {
+    static ID = 'ui.component.QRCodeView';
+
     static PROPERTIES = {
         value: {
             description: 'The text to encode in the QR code',
@@ -76,6 +78,4 @@ export default class QRCodeView extends Component {
             }
         });
     }
-}
-
-defineComponent('c-qr-code', QRCodeView);
+});

+ 4 - 5
src/UI/Components/RecoveryCodeEntryView.js

@@ -1,6 +1,7 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
-export default class RecoveryCodeEntryView extends Component {
+export default def(class RecoveryCodeEntryView extends Component {
+    static ID = 'ui.component.RecoveryCodeEntryView';
     static PROPERTIES = {
         value: {},
         length: { value: 8 },
@@ -82,6 +83,4 @@ export default class RecoveryCodeEntryView extends Component {
             }
         });
     }
-}
-
-defineComponent('c-recovery-code-entry', RecoveryCodeEntryView);
+});

+ 5 - 5
src/UI/Components/RecoveryCodesView.js

@@ -1,6 +1,8 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
+
+export default def(class RecoveryCodesView extends Component {
+    static ID = 'ui.component.RecoveryCodesView';
 
-export default class RecoveryCodesView extends Component {
     static PROPERTIES = {
         values: {
             description: 'The recovery codes to display',
@@ -89,6 +91,4 @@ export default class RecoveryCodesView extends Component {
             print_frame.contentWindow.window.print();
         });
     }
-}
-
-defineComponent('c-recovery-codes-view', RecoveryCodesView);
+});

+ 5 - 5
src/UI/Components/Slider.js

@@ -16,12 +16,14 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * Slider: A labeled slider input.
  */
-export default class Slider extends Component {
+export default def(class Slider extends Component {
+    static ID = 'ui.component.Slider';
+
     static PROPERTIES = {
         name: { value: null },
         label: { value: null },
@@ -109,6 +111,4 @@ export default class Slider extends Component {
             input.value = value;
         });
     }
-}
-
-defineComponent('c-slider', Slider);
+});

+ 41 - 0
src/UI/Components/Spinner.js

@@ -0,0 +1,41 @@
+const Component = use('util.Component');
+
+export default def(class Spinner extends Component {
+    static ID = 'ui.component.Spinner';
+
+    static PROPERTIES = {}
+    // static RENDER_MODE = Component.NO_SHADOW;
+
+    create_template ({ template }) {
+        console.log('template?', template);
+
+        template.innerHTML = /*html*/`
+            <div>
+                <svg style="display:block; margin: 0 auto; " xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
+                    <title>circle anim</title>
+                    <g fill="#212121" class="nc-icon-wrapper">
+                        <g class="nc-loop-circle-24-icon-f">
+                            <path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path>
+                            <path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path>
+                        </g>
+                        <style>
+                            .nc-loop-circle-24-icon-f{
+                                --animation-duration:0.5s;
+                                transform-origin:12px 12px;
+                                animation:nc-loop-circle-anim var(--animation-duration) infinite linear
+                            }
+                            @keyframes nc-loop-circle-anim{
+                                0%{
+                                    transform:rotate(0)
+                                }
+                                100%{
+                                    transform:rotate(360deg)
+                                }
+                            }
+                        </style>
+                    </g>
+                </svg>
+            </div>
+        `;
+    }
+});

+ 5 - 5
src/UI/Components/StepHeading.js

@@ -1,11 +1,13 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * StepHeading renders a heading with a leading symbol.
  * The leading symbol is styled inside a cricle and is
  * optimized for single-digit numbers.
  */
-export default class StepHeading extends Component {
+export default def(class StepHeading extends Component {
+    static ID = 'ui.component.StepHeading';
+
     static PROPERTIES = {
         symbol: {
             description: 'The symbol to display',
@@ -56,6 +58,4 @@ export default class StepHeading extends Component {
             </div>
         `);
     }
-}
-
-defineComponent('c-step-heading', StepHeading);
+});

+ 5 - 5
src/UI/Components/StepView.js

@@ -1,6 +1,8 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
+
+export default def(class StepView extends Component {
+    static ID = 'ui.component.StepView';
 
-export default class StepView extends Component {
     static PROPERTIES = {
         children: {},
         done: { value: false },
@@ -62,6 +64,4 @@ export default class StepView extends Component {
         }
         this.set('position', this.get('position') + 1);
     }
-}
-
-defineComponent('c-step-view', StepView);
+});

+ 5 - 5
src/UI/Components/StringView.js

@@ -1,10 +1,12 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * A simple component that displays a string in the
  * specified style.
  */
-export default class StringView extends Component {
+export default def(class StringView extends Component {
+    static ID = 'ui.component.StringView';
+
     static PROPERTIES = {
         text: { value: '' },
         heading: { value: 0 },
@@ -40,6 +42,4 @@ export default class StringView extends Component {
             either({ heading: this.get('heading'), text });
         });
     }
-}
-
-defineComponent('c-string-view', StringView);
+});

+ 5 - 5
src/UI/Components/Table.js

@@ -1,9 +1,11 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * A table with a sticky header
  */
-export default class Table extends Component {
+export default def(class Table extends Component {
+    static ID = 'ui.component.Table';
+
     static PROPERTIES = {
         headings: { value: [] },
         scale: { value: '2pt' },
@@ -78,6 +80,4 @@ export default class Table extends Component {
             }
         });
     }
-}
-
-defineComponent('c-table', Table);
+});

+ 5 - 5
src/UI/Components/TestView.js

@@ -1,9 +1,11 @@
-import { Component, defineComponent } from "../../util/Component.js";
+const Component = use('util.Component');
 
 /**
  * A simple component when you just need to test something.
  */
-export default class TestView extends Component {
+export default def(class TestView extends Component {
+    static ID = 'ui.component.TestView';
+
     static CSS = `
         div {
             background-color: lightblue;
@@ -17,6 +19,4 @@ export default class TestView extends Component {
             <div>I am a test view</div>
         `);
     }
-}
-
-defineComponent('c-test-view', TestView);
+});

+ 5 - 2
src/UI/UIWindowTaskManager.js

@@ -49,6 +49,7 @@ const end_process = async (uuid, force) => {
 };
 
 class TaskManagerTable extends Component {
+    static ID = 'ui.component.TaskManagerTable';
     static PROPERTIES = {
         tasks: { value: [] },
     };
@@ -157,9 +158,11 @@ class TaskManagerTable extends Component {
         return rows;
     };
 }
-defineComponent('c-task-manager-table', TaskManagerTable);
+defineComponent(TaskManagerTable);
 
 class TaskManagerRow extends Component {
+    static ID = 'ui.component.TaskManagerRow';
+
     static PROPERTIES = {
         name: {},
         uuid: {},
@@ -291,7 +294,7 @@ class TaskManagerRow extends Component {
         });
     }
 }
-defineComponent('c-task-manager-row', TaskManagerRow);
+defineComponent(TaskManagerRow);
 
 const UIWindowTaskManager = async function UIWindowTaskManager () {
     const svc_process = globalThis.services.get('process');

+ 4 - 0
src/definitions.js

@@ -17,6 +17,10 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 export class Service {
+    construct () {
+        if ( ! this._construct ) return;
+        return this._construct();
+    }
     init (...a) {
         if ( ! this._init ) return;
         return this._init(...a)

+ 1 - 17
src/index.js

@@ -16,25 +16,9 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-window.service_script_api_promise = (() => {
-    let resolve, reject;
-    const promise = new Promise((res, rej) => {
-        resolve = res;
-        reject = rej;
-    });
-    promise.resolve = resolve;
-    promise.reject = reject;
-    return promise;
-})();
-window.service_script = async fn => {
-    try {
-        await fn(await window.service_script_api_promise);
-    } catch (e) {
-        console.error('service_script(ERROR)', e);
-    }
-};
 
 window.puter_gui_enabled = true;
+
 /**
  * Initializes and configures the GUI (Graphical User Interface) settings based on the provided options.
  * 

+ 8 - 0
src/init_async.js

@@ -0,0 +1,8 @@
+// Note: this logs AFTER all imports because imports are hoisted
+logger.info('start -> async initialization');
+
+import './util/TeePromise.js';
+import './util/Component.js';
+
+logger.info('end -> async initialization');
+globalThis.init_promise.resolve();

+ 137 - 0
src/init_sync.js

@@ -0,0 +1,137 @@
+/**
+ * @global
+ * @function logger
+ * @param {Array<any>} a - The arguments.
+ */
+/**
+ * @global
+ * @function use
+ * @param {string} arg - The string argument.
+ * @returns {any} The return value.
+ */
+/**
+ * @global
+ * @function def
+ * @param {any} arg - The argument.
+ * @returns {any} The return value.
+ */
+
+// An initial logger to log do before we get a more fancy logger
+// (which we never really do yet, at the time of writing this);
+// something like this was also done in backend and it proved useful.
+(scope => {
+    globalThis.logger = {
+        info: (...a) => console.log('%c[INIT/INFO]', 'color: #4287f5', ...a),
+    };
+})(globalThis);
+logger.info('start -> blocking initialization');
+
+// A global promise (like TeePromise, except we can't import anything yet)
+// that will be resolved by `init_async.js` when it completes.
+(scope => {
+    scope.init_promise = (() => {
+        let resolve, reject;
+        const promise = new Promise((res, rej) => {
+            resolve = res;
+            reject = rej;
+        });
+        promise.resolve = resolve;
+        promise.reject = reject;
+        return promise;
+    })();
+})(globalThis);
+
+// This is where `use()` and `def()` are defined.
+//
+// A global registry for class definitions. This allows us to expose
+// classes to service scripts even when the frontend code is bundled.
+// Additionally, it allows us to create hooks upon class registration,
+// which we use to turn classes which extend HTMLElement into components
+// (i.e. give them tag names because that is required).
+//
+// It's worth noting `use()` and `def()` for service scripts is exposed
+// in initgui.js, in the `launch_services()` function. (at the time this
+// comment was written)
+(scope => {
+    const registry_ = {
+        classes_m: {},
+        classes_l: [],
+        hooks_on_register: [],
+    };
+
+    const on_self_registered_api = {
+        on_other_registered: hook => registry_.hooks_on_register.push(hook),
+    }
+
+    scope.lib = {
+        is_subclass (subclass, superclass) {
+            if (subclass === superclass) return true;
+
+            let proto = subclass.prototype;
+            while (proto) {
+                if (proto === superclass.prototype) return true;
+                proto = Object.getPrototypeOf(proto);
+            }
+
+            return false;
+        }
+    };
+
+    scope.def = (cls, id) => {
+        id = id || cls.ID;
+        if ( id === undefined ) {
+            throw new Error('Class must have an ID');
+        }
+
+        if ( registry_.classes_m[id] ) {
+            throw new Error(`Class with ID ${id} already registered`);
+        }
+
+        registry_.classes_m[id] = cls;
+        registry_.classes_l.push(cls);
+
+        registry_.hooks_on_register.forEach(hook => hook({ cls }));
+
+        console.log('registered class', id, registry_);
+
+        // Find class that owns 'on_self_registered' hook
+        let owner = cls;
+        while (
+            owner.__proto__ && owner.__proto__.on_self_registered
+            && owner.__proto__.on_self_registered === cls.on_self_registered
+        ) {
+            owner = owner.__proto__;
+        }
+
+        if ( cls.on_self_registered ) {
+            cls.on_self_registered.call(cls, {
+                ...on_self_registered_api,
+                is_owner: cls === owner,
+            });
+        }
+
+        return cls;
+    };
+
+    scope.use = id => {
+        console.log('use called with id: ', id);
+        if ( id === undefined ) {
+            return registry_.classes_m;
+        }
+
+        if ( !registry_.classes_m[id] ) {
+            throw new Error(`Class with ID ${id} not registered`);
+        }
+
+        console.log(
+            'okay it\'s going to return:',
+            registry_.classes_m[id],
+            'and the entire map is this: ',
+            registry_.classes_m
+        )
+
+        return registry_.classes_m[id];
+    }
+})(globalThis);
+
+logger.info('end -> blocking initialization');

+ 15 - 2
src/initgui.js

@@ -40,7 +40,10 @@ import { ProcessService } from './services/ProcessService.js';
 import { PROCESS_RUNNING } from './definitions.js';
 import { LocaleService } from './services/LocaleService.js';
 import { SettingsService } from './services/SettingsService.js';
+
 import UIComponentWindow from './UI/UIComponentWindow.js';
+import Spinner from './UI/Components/Spinner.js';
+
 
 const launch_services = async function () {
     // === Services Data Structures ===
@@ -54,6 +57,8 @@ const launch_services = async function () {
         services_m_[name] = instance;
     }
 
+    globalThis.def(UIComponentWindow, 'ui.UIComponentWindow');
+
     // === Hooks for Service Scripts from Backend ===
     const service_script_deferred = { services: [], on_ready: [] };
     const service_script_api = {
@@ -61,7 +66,9 @@ const launch_services = async function () {
         on_ready: fn => service_script_deferred.on_ready.push(fn),
         // Some files can't be imported by service scripts,
         // so this hack makes that possible.
-        use: name => ({ UIWindow, UIComponentWindow })[name],
+        def: globalThis.def,
+        use: globalThis.use,
+        // use: name => ({ UIWindow, UIComponentWindow })[name],
     };
     globalThis.service_script_api_promise.resolve(service_script_api);
 
@@ -78,7 +85,13 @@ const launch_services = async function () {
     }
 
     for (const [_, instance] of services_l_) {
-        await instance.init();
+        await instance.construct();
+    }
+
+    for (const [_, instance] of services_l_) {
+        await instance.init({
+            services: globalThis.services,
+        });
     }
 
     // === Service-Script Ready ===

+ 13 - 0
src/services/ExportRegistrantService.js

@@ -0,0 +1,13 @@
+import Spinner from "../UI/Components/Spinner";
+import { Service } from "../definitions";
+
+/**
+ * This class exists to keep exports to the service script API separate
+ * from the service where exports are registered. This will make it easier
+ * to change how it works in the future.
+ */
+export class ExportRegistrantService extends Service {
+    _init () {
+        console.log(Spinner); // import gets optimized out if we don't do this
+    }
+}

+ 2 - 0
src/static-assets.js

@@ -44,6 +44,8 @@ const css_paths = [
 
 // Ordered list of JS scripts
 const js_paths = [
+    '/init_sync.js',
+    '/init_async.js',
     '/initgui.js',
     '/helpers.js',
     '/IPC.js',

+ 38 - 6
src/util/Component.js

@@ -1,6 +1,8 @@
 import ValueHolder from "./ValueHolder.js";
 
-export class Component extends HTMLElement {
+export const Component = def(class Component extends HTMLElement {
+    static ID = 'util.Component';
+
     #has_created_element = false;
     #has_called_on_ready = false;
 
@@ -11,6 +13,27 @@ export class Component extends HTMLElement {
         'value bindings for create_template',
     ]
 
+    static on_self_registered ({ is_owner, on_other_registered }) {
+        // Only invoked for Component itself, not subclasses
+        if ( ! is_owner ) return;
+
+        // Automatically define components for all HTML elements
+        on_other_registered(({ cls }) => {
+            console.log('detected class', cls.ID);
+            if ( cls.ID === 'ui.component.StepHeading' ) {
+                globalThis.sh_shouldbe = cls;
+                console.log(
+                    'this is what StepHeading should be',
+                    cls
+                );
+            }
+            if ( globalThis.lib.is_subclass(cls, HTMLElement) ) {
+                console.log('registering as an element');
+                defineComponent(cls);
+            }
+        });
+    }
+
     constructor (property_values) {
         super();
 
@@ -144,12 +167,21 @@ export class Component extends HTMLElement {
             }
         };
     }
-}
+});
+
+export const defineComponent = (component) => {
+    // Web components need tags (despite that we never use the tags)
+    // because it was designed this way.
+    if ( globalThis.lib.is_subclass(component, HTMLElement) ) {
+        let name = component.ID;
+        name = 'c-' + name.split('.').pop().toLowerCase();
+        // TODO: This is necessary because files can be loaded from
+        // both `/src/UI` and `/UI` in the URL; we need to fix that
+        if ( customElements.get(name) ) return;
+
+        // console.log('[surely] defining', name, 'as', component);
 
-export const defineComponent = (name, component) => {
-    // TODO: This is necessary because files can be loaded from
-    // both `/src/UI` and `/UI` in the URL; we need to fix that
-    if ( ! customElements.get(name) ) {
         customElements.define(name, component);
+        component.defined_as = name;
     }
 };

+ 4 - 2
src/util/TeePromise.js

@@ -1,4 +1,6 @@
-export default class TeePromise {
+export default def(class TeePromise {
+    static ID = 'util.TeePromise';
+
     static STATUS_PENDING = {};
     static STATUS_RUNNING = {};
     static STATUS_DONE = {};
@@ -40,4 +42,4 @@ export default class TeePromise {
     onComplete(fn) {
         return this.then(fn);
     }
-}
+});