From b2b29c61a9c13c15d9ef4092eb516e4aa0d93123 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sat, 21 Feb 2026 21:04:43 -0500 Subject: [PATCH] extensions: Simplify code used for loading xlets, improve startup speed, fix backtrace uselessness. - Drop 'custom' importer code, use native cjs importer - Leave compatibility functions for require() and module.exports Fixes/improves: - Faster Cinnamon startup (for my random configuration, ~1550ms down to ~1200ms). - Improved logging. - More consistent code (use of deprecated features will start getting flagged and/or blocked for PRs in Cinnamon and spice repositories. - Previous code was practically unmaintainable by sane people. Logging - where before we'd see this useless garbage: (cinnamon:4334): St-CRITICAL **: 22:19:57.081: st_widget_get_theme_node called on the widget [0x622f6579b3c0 StLabel.hourly-data ("...")] which is not in the stage. == Stack trace for context 0x622f63105b50 == 0 7ffd484001c0 b /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19086 (359f8e6c9c0 @ 279) 1 622f632b5d08 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19059 (359f8e6c970 @ 23) 2 7ffd48400ca0 b /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:18976 (359f8e6c6a0 @ 807) 3 622f632b5b68 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19550 (359f8e6d650 @ 104) 4 622f632b5ab8 i /usr/share/cinnamon/js/misc/fileUtils.js line 211 > Function:19842 (359f8e6e2e0 @ 681) 5 622f632b5a08 i self-hosted:1461 (1eb5245b46f0 @ 30) 6 7ffd484015a0 b self-hosted:852 (359f8e92dd0 @ 15) we now get: (cinnamon:4334): St-CRITICAL **: 22:20:36.263: st_widget_get_theme_node called on the widget [0x58cf71ab5180 StLabel.hourly-data ("...")] which is not in the stage. == Stack trace for context 0x58cf6ee74250 == 0 7fffae294e10 b /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19084 (387d8e1fd1a0 @ 279) 1 58cf6eeac1f8 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19057 (387d8e1fd150 @ 23) 2 7fffae2958f0 b /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:18974 (387d8e1fce20 @ 807) 3 58cf6eeac058 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19548 (387d8e1fddd0 @ 104) 4 58cf6eeabfa8 i /home/mtwebster/.local/share/cinnamon/applets/weather@mockturtl/3.8/weather-applet.js:19840 (387d8e1fea60 @ 681) 5 58cf6eeabef8 i self-hosted:1461 (2daaca0bf470 @ 30) 6 7fffae2961f0 b self-hosted:852 (e85adf8c6a0 @ 15) Requires https://github.com/linuxmint/cjs/pull/136 for xlet 'reload' functionality to work. --- .../applets/calendar@cinnamon.org/applet.js | 6 +- .../applets/calendar@cinnamon.org/calendar.js | 2 +- .../calendar@cinnamon.org/eventView.js | 2 +- .../appGroup.js | 11 +- .../applet.js | 15 +- .../constants.js | 51 +- .../grouped-window-list@cinnamon.org/menus.js | 15 +- .../grouped-window-list@cinnamon.org/state.js | 6 +- .../workspace.js | 11 +- .../applets/menu@cinnamon.org/applet.js | 3 +- js/misc/fileUtils.js | 212 -------- js/ui/appletManager.js | 6 +- js/ui/deskletManager.js | 3 +- js/ui/extension.js | 514 +++++++++++++----- js/ui/extensionSystem.js | 11 +- js/ui/main.js | 6 + js/ui/searchProviderManager.js | 3 +- 17 files changed, 454 insertions(+), 423 deletions(-) diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js index 7b9af66822..436bce96b3 100644 --- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/applet.js @@ -8,12 +8,14 @@ const Util = imports.misc.util; const PopupMenu = imports.ui.popupMenu; const UPowerGlib = imports.gi.UPowerGlib; const Settings = imports.ui.settings; -const Calendar = require('./calendar'); -const EventView = require('./eventView'); const CinnamonDesktop = imports.gi.CinnamonDesktop; const Main = imports.ui.main; const Separator = imports.ui.separator; +const Me = imports.ui.extension.getCurrentExtension(); +const Calendar = Me.imports.calendar; +const EventView = Me.imports.eventView; + const DAY_FORMAT = CinnamonDesktop.WallClock.lctime_format("cinnamon", "%A"); const DATE_FORMAT_SHORT = CinnamonDesktop.WallClock.lctime_format("cinnamon", _("%B %-e, %Y")); const DATE_FORMAT_FULL = CinnamonDesktop.WallClock.lctime_format("cinnamon", _("%A, %B %-e, %Y")); diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js index 05c9ddfed0..24bc303bf9 100644 --- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js +++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/calendar.js @@ -145,7 +145,7 @@ function _dateIntervalsOverlap(a0, a1, b0, b1) return true; } -class Calendar { +var Calendar = class Calendar { constructor(settings, events_manager) { this.events_manager = events_manager; this._weekStart = Cinnamon.util_get_week_start(); diff --git a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js index 9f3aec2254..ad23d4d08a 100644 --- a/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js +++ b/files/usr/share/cinnamon/applets/calendar@cinnamon.org/eventView.js @@ -290,7 +290,7 @@ class EventDataList { } } -class EventsManager { +var EventsManager = class EventsManager { constructor(settings, desktop_settings) { this.settings = settings; this.desktop_settings = desktop_settings; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index 8ca0467b56..1a12a801bc 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -12,8 +12,9 @@ const Mainloop = imports.mainloop; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; -const createStore = require('./state'); -const {AppMenuButtonRightClickMenu, HoverMenuController, AppThumbnailHoverMenu} = require('./menus'); +const Me = imports.ui.extension.getCurrentExtension(); +const {createStore} = Me.imports.state; +const {AppMenuButtonRightClickMenu, HoverMenuController, AppThumbnailHoverMenu} = Me.imports.menus; const { FLASH_INTERVAL, FLASH_MAX_COUNT, @@ -21,7 +22,7 @@ const { BUTTON_BOX_ANIMATION_TIME, RESERVE_KEYS, TitleDisplay -} = require('./constants'); +} = Me.imports.constants; const _reLetterRtl = new RegExp("\\p{Script=Hebrew}|\\p{Script=Arabic}", "u"); const _reLetter = new RegExp("\\p{L}", "u"); @@ -60,7 +61,7 @@ const getFocusState = function(metaWindow) { return false; }; -class AppGroup { +var AppGroup = class AppGroup { constructor(params) { this.state = params.state; this.workspaceState = params.workspaceState; @@ -1224,5 +1225,3 @@ class AppGroup { } } } - -module.exports = AppGroup; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 24c944e17c..ba61cc1af5 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -14,14 +14,15 @@ const {AppletSettings} = imports.ui.settings; const {SignalManager} = imports.misc.signalManager; const {throttle, unref, trySpawnCommandLine} = imports.misc.util; -const createStore = require('./state'); -const AppGroup = require('./appGroup'); -const Workspace = require('./workspace'); +const Me = imports.ui.extension.getCurrentExtension(); +const {createStore} = Me.imports.state; +const {AppGroup} = Me.imports.appGroup; +const {Workspace} = Me.imports.workspace; const { - RESERVE_KEYS, - TitleDisplay, - autoStartStrDir -} = require('./constants'); + RESERVE_KEYS, + TitleDisplay, + autoStartStrDir +} = Me.imports.constants; class PinnedFavs { constructor(params) { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js index 51b08467e4..4395c17bb0 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js @@ -1,29 +1,24 @@ -const CLOSE_BTN_SIZE = 22; -const constants = { - CLOSE_BTN_SIZE, - CLOSED_BUTTON_STYLE: 'padding: 0px; width: ' + CLOSE_BTN_SIZE + 'px; height: ' - + CLOSE_BTN_SIZE + 'px; max-width: ' + CLOSE_BTN_SIZE - + 'px; max-height: ' + CLOSE_BTN_SIZE + 'px; ' + '-cinnamon-close-overlap: 0px;' + - 'background-size: ' + CLOSE_BTN_SIZE + 'px ' + CLOSE_BTN_SIZE + 'px;', - THUMBNAIL_ICON_SIZE: 16, - OPACITY_OPAQUE: 255, - BUTTON_BOX_ANIMATION_TIME: 150, - MAX_BUTTON_WIDTH: 150, // Pixels - FLASH_INTERVAL: 500, - FLASH_MAX_COUNT: 4, - RESERVE_KEYS: ['willUnmount'], - TitleDisplay: { - None: 1, - App: 2, - Title: 3, - Focused: 4 - }, - FavType: { - favorites: 0, - pinnedApps: 1, - none: 2 - }, - autoStartStrDir: './.config/autostart', +var CLOSE_BTN_SIZE = 22; +var CLOSED_BUTTON_STYLE = 'padding: 0px; width: ' + CLOSE_BTN_SIZE + 'px; height: ' + + CLOSE_BTN_SIZE + 'px; max-width: ' + CLOSE_BTN_SIZE + + 'px; max-height: ' + CLOSE_BTN_SIZE + 'px; ' + '-cinnamon-close-overlap: 0px;' + + 'background-size: ' + CLOSE_BTN_SIZE + 'px ' + CLOSE_BTN_SIZE + 'px;'; +var THUMBNAIL_ICON_SIZE = 16; +var OPACITY_OPAQUE = 255; +var BUTTON_BOX_ANIMATION_TIME = 150; +var MAX_BUTTON_WIDTH = 150; // Pixels +var FLASH_INTERVAL = 500; +var FLASH_MAX_COUNT = 4; +var RESERVE_KEYS = ['willUnmount']; +var TitleDisplay = { + None: 1, + App: 2, + Title: 3, + Focused: 4 }; - -module.exports = constants; +var FavType = { + favorites: 0, + pinnedApps: 1, + none: 2 +}; +var autoStartStrDir = './.config/autostart'; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js index e176f20163..607b9cbd32 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/menus.js @@ -10,6 +10,7 @@ const WindowUtils = imports.misc.windowUtils; const Mainloop = imports.mainloop; const {tryFn, unref, trySpawnCommandLine, spawn_async, getDesktopActionIcon} = imports.misc.util; +const Me = imports.ui.extension.getCurrentExtension(); const { CLOSE_BTN_SIZE, CLOSED_BUTTON_STYLE, @@ -17,7 +18,7 @@ const { RESERVE_KEYS, FavType, autoStartStrDir -} = require('./constants'); +} = Me.imports.constants; const convertRange = function(value, r1, r2) { return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0]; @@ -39,7 +40,7 @@ const setOpacity = (peekTime, window_actor, targetOpacity, cb) => { window_actor.ease(easeConfig); }; -class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu { +var AppMenuButtonRightClickMenu = class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu { constructor(params, orientation) { super(params, orientation); this.state = params.state; @@ -413,7 +414,7 @@ class AppMenuButtonRightClickMenu extends Applet.AppletPopupMenu { } } -class HoverMenuController extends PopupMenu.PopupMenuManager { +var HoverMenuController = class HoverMenuController extends PopupMenu.PopupMenuManager { constructor(actor, groupState) { super({actor}, false); // owner, shouldGrab this.groupState = groupState; @@ -860,7 +861,7 @@ class WindowThumbnail { } } -class AppThumbnailHoverMenu extends PopupMenu.PopupMenu { +var AppThumbnailHoverMenu = class AppThumbnailHoverMenu extends PopupMenu.PopupMenu { _init(state, groupState) { super._init.call(this, groupState.trigger('getActor'), state.orientation, 0.5); this.state = state; @@ -1227,9 +1228,3 @@ class AppThumbnailHoverMenu extends PopupMenu.PopupMenu { unref(this, RESERVE_KEYS); } } - -module.exports = { - AppMenuButtonRightClickMenu, - HoverMenuController, - AppThumbnailHoverMenu -}; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js index 714422334f..758e1d9a9e 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/state.js @@ -137,7 +137,7 @@ function clone(object, refs = [], cache = null) { * to be used at the end of the application life cycle. * */ -function createStore(state = {}, listeners = [], connections = 0) { +var createStore = function(state = {}, listeners = [], connections = 0) { const publicAPI = Object.freeze({ get, set, @@ -316,6 +316,4 @@ function createStore(state = {}, listeners = [], connections = 0) { } return getAPIWithObject(state); -} - -module.exports = createStore; \ No newline at end of file +}; \ No newline at end of file diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 4af591ee8c..ff25863aa3 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -3,11 +3,12 @@ const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; -const createStore = require('./state'); -const AppGroup = require('./appGroup'); -const {RESERVE_KEYS} = require('./constants'); +const Me = imports.ui.extension.getCurrentExtension(); +const {createStore} = Me.imports.state; +const {AppGroup} = Me.imports.appGroup; +const {RESERVE_KEYS} = Me.imports.constants; -class Workspace { +var Workspace = class Workspace { constructor(params) { this.state = params.state; this.state.connect({ @@ -428,5 +429,3 @@ class Workspace { unref(this, RESERVE_KEYS); } } - -module.exports = Workspace; diff --git a/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js index dbeb75fc77..28b48126e7 100644 --- a/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/menu@cinnamon.org/applet.js @@ -34,7 +34,8 @@ const USER_DESKTOP_PATH = FileUtils.getUserDesktopDir(); const PRIVACY_SCHEMA = "org.cinnamon.desktop.privacy"; const REMEMBER_RECENT_KEY = "remember-recent-files"; -const AppUtils = require('./appUtils'); +const Me = imports.ui.extension.getCurrentExtension(); +const AppUtils = Me.imports.appUtils; let appsys = Cinnamon.AppSystem.get_default(); diff --git a/js/misc/fileUtils.js b/js/misc/fileUtils.js index 3374c49291..b7e03519aa 100644 --- a/js/misc/fileUtils.js +++ b/js/misc/fileUtils.js @@ -4,34 +4,6 @@ const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const ByteArray = imports.byteArray; -var importNames = [ - 'mainloop', - 'jsUnit', - 'format', - 'signals', - 'lang', - 'tweener', - 'overrides', - 'gettext', - 'coverage', - 'package', - 'cairo', - 'byteArray', - 'cairoNative' -]; -var cinnamonImportNames = [ - 'ui', - 'misc', - 'perf' -]; -var giImportNames = imports.gi.GIRepository.Repository - .get_default() - .get_loaded_namespaces(); -var LoadedModules = []; -var FunctionConstructor = Symbol(); -var Symbols = {}; -Symbols[FunctionConstructor] = 0..constructor.constructor; - function listDirAsync(file, callback) { let allFiles = []; file.enumerate_children_async(Gio.FILE_ATTRIBUTE_STANDARD_NAME, @@ -111,187 +83,3 @@ function getUserDesktopDir() { if (file.query_exists(null)) return path; else return null; } - -function findModuleIndex(path) { - return LoadedModules.findIndex(function(cachedModule) { - return cachedModule && cachedModule.path === path; - }); -} - -function getModuleByIndex(index) { - if (!LoadedModules[index]) { - throw new Error('[getModuleByIndex] Module does not exist.'); - } - return LoadedModules[index].module; -} - -function unloadModule(index) { - if (!LoadedModules[index]) { - return; - } - let indexes = []; - for (let i = 0; i < LoadedModules.length; i++) { - if (LoadedModules[i] && LoadedModules[i].dir === LoadedModules[index].dir) { - indexes.push(i); - } - } - for (var i = 0; i < indexes.length; i++) { - LoadedModules[indexes[i]].module = undefined; - LoadedModules[indexes[i]].size = -1; - } -} - -function createExports({path, dir, meta, type, file, size, JS, returnIndex, reject}) { - // Import data is stored in an array of objects and the module index is looked up by path. - var importerData = { // changed from 'let' to 'var'. - size, - path, - dir, - module: null - }; - // module.exports as an object holding a module's namespaces is a node convention, and is intended - // to help interop with other libraries. - var exports = {}; // changed from 'const' to 'var'. - var module = { // changed from 'const' to 'var'. - exports: exports - }; - - // Storing by array index that other extension classes can look up. - let moduleIndex = findModuleIndex(path); - if (moduleIndex > -1) { - // Module already exists, check if its been updated - if (size === LoadedModules[moduleIndex].size - && LoadedModules[moduleIndex].module != null) { - // Return the cache - return returnIndex ? moduleIndex : LoadedModules[moduleIndex].module; - } - // Module has been updated - LoadedModules[moduleIndex] = importerData; - } else { - LoadedModules.push(importerData); - moduleIndex = LoadedModules.length - 1; - } - - JS = `'use strict';${JS};`; - // Regex matches the top level variable names, and appends them to the module.exports object, - // mimicking the native CJS importer. - const exportsRegex = /^module\.exports(\.[a-zA-Z0-9_$]+)?\s*=/m; - const varRegex = /^(?:'use strict';){0,}(const|var|let|function|class)\s+([a-zA-Z0-9_$]+)/gm; - var match; // changed from 'let' to 'var'. - - if (!exportsRegex.test(JS)) { - while ((match = varRegex.exec(JS)) != null) { - if (match.index === varRegex.lastIndex) { - varRegex.lastIndex++; - } - // Don't modularize native imports - if (match[2] - && importNames.indexOf(match[2].toLowerCase()) === -1 - && giImportNames.indexOf(match[2]) === -1) { - JS += `exports.${match[2]} = typeof ${match[2]} !== 'undefined' ? ${match[2]} : null;`; - } - } - } - - // send_results is overridden in SearchProviderManager, so we need to make sure the send_results - // function on the exports object, what SearchProviderManager actually has access to outside the - // module scope, is called. - if (type === 'search_provider') { - JS += 'var send_results = function() {exports.send_results.apply(this, arguments)};' + - 'var get_locale_string = function() {return exports.get_locale_string.apply(this, arguments)};'; - } - - // Return the exports object containing all of our top level namespaces, and include the sourceURL so - // Spidermonkey includes the file names in stack traces. - JS += `return module.exports;//# sourceURL=${path}`; - - try { - // Create the function returning module.exports and return it to Extension so it can be called by the - // appropriate manager. - importerData.module = Symbols[FunctionConstructor]( - 'require', - 'exports', - 'module', - '__meta', - '__dirname', - '__filename', - JS - ).call( - exports, - function require(path) { - return requireModule(path, dir, meta, type); - }, - exports, - module, - meta, - dir, - file.get_basename() - ); - - return returnIndex ? moduleIndex : importerData.module; - } catch (e) { - if (reject) { - reject(e); - return; - } - throw e; - } -} - -function requireModule(path, dir, meta, type, async = false, returnIndex = false) { - // Allow passing through native bindings, e.g. const Cinnamon = require('gi.Cinnamon'); - // Check if this is a GI import - if (path.substr(0, 3) === 'gi.') { - return imports.gi[path.substr(3, path.length)]; - } - // Check if this is a Cinnamon import - let importPrefix = path.split('.')[0]; - if (cinnamonImportNames.indexOf(importPrefix) > -1 - && path.substr(0, importPrefix.length + 1) === `${importPrefix}.`) { - return imports[importPrefix][path.substr(importPrefix.length + 1, path.length)]; - } - // Check if this is a top level import - if (importNames.indexOf(path) > -1) { - return imports[path]; - } - // Check the file extension - if (path.substr(-3) !== '.js') { - path += '.js'; - } - // Check relative paths - if (path[0] === '.' || path[0] !== '/') { - path = path.replace(/\.\//g, ''); - if (dir) { - path = dir + "/" + path; - } - } - let success, JSbytes, JS; - let file = Gio.File.new_for_commandline_arg(path); - let fileLoadErrorMessage = '[requireModule] Unable to load file contents.'; - if (!file.query_exists(null)) { - throw new Error("[requireModule] Path does not exist.\n" + path); - } - - if (!async) { - [success, JSbytes] = file.load_contents(null); - if (!success) { - throw new Error(fileLoadErrorMessage); - } - JS = ByteArray.toString(JSbytes); - return createExports({path, dir, meta, type, file, size: JS.length, JS, returnIndex}); - } - return new Promise(function(resolve, reject) { - file.load_contents_async(null, function(object, result) { - try { - [success, JSbytes] = file.load_contents_finish(result); - if (!success) { - throw new Error(fileLoadErrorMessage); - } - JS = ByteArray.toString(JSbytes); - resolve(createExports({path, dir, meta, type, file, size: JS.length, JS, returnIndex, reject})); - } catch (e) { - reject(e); - } - }); - }); -} diff --git a/js/ui/appletManager.js b/js/ui/appletManager.js index b8bfc679d1..b7338f4fa2 100644 --- a/js/ui/appletManager.js +++ b/js/ui/appletManager.js @@ -10,7 +10,6 @@ const Applet = imports.ui.applet; const Extension = imports.ui.extension; const ModalDialog = imports.ui.modalDialog; const Dialog = imports.ui.dialog; -const {getModuleByIndex} = imports.misc.fileUtils; const {queryCollection} = imports.misc.util; const Gettext = imports.gettext; const Panel = imports.ui.panel; @@ -592,14 +591,13 @@ function createApplet(extension, appletDefinition, panel = null) { let applet; try { - let module = getModuleByIndex(extension.moduleIndex); - if (!module) { + if (!extension.module) { return null; } // FIXME: Panel height is now available before an applet is initialized, // so we don't need to pass it to the constructor anymore, but would // require a compatibility clean-up effort. - applet = module.main(extension.meta, orientation, panel.height, applet_id); + applet = extension.module.main(extension.meta, orientation, panel.height, applet_id); } catch (e) { Extension.logError(`Failed to evaluate 'main' function on applet: ${uuid}/${applet_id}`, uuid, e); return null; diff --git a/js/ui/deskletManager.js b/js/ui/deskletManager.js index 07bd619468..8676b9edea 100644 --- a/js/ui/deskletManager.js +++ b/js/ui/deskletManager.js @@ -12,7 +12,6 @@ const Desklet = imports.ui.desklet; const DND = imports.ui.dnd; const Extension = imports.ui.extension; const Main = imports.ui.main; -const {getModuleByIndex} = imports.misc.fileUtils; const {queryCollection} = imports.misc.util; // Maps uuid -> importer object (desklet directory tree) @@ -333,7 +332,7 @@ function _createDesklets(extension, deskletDefinition) { let desklet; try { - desklet = getModuleByIndex(extension.moduleIndex).main(extension.meta, desklet_id); + desklet = extension.module.main(extension.meta, desklet_id); } catch (e) { Extension.logError('Failed to evaluate \'main\' function on desklet: ' + uuid + "/" + desklet_id, e); return null; diff --git a/js/ui/extension.js b/js/ui/extension.js index 08197a7492..4a8b8b6493 100644 --- a/js/ui/extension.js +++ b/js/ui/extension.js @@ -15,7 +15,6 @@ const DeskletManager = imports.ui.deskletManager; const ExtensionSystem = imports.ui.extensionSystem; const SearchProviderManager = imports.ui.searchProviderManager; const Main = imports.ui.main; -const {requireModule, unloadModule, getModuleByIndex} = imports.misc.fileUtils; const {queryCollection} = imports.misc.util; var State = { @@ -26,17 +25,6 @@ var State = { X11_ONLY: 4 }; -// Xlets using imports.gi.NMClient. This should be removed in Cinnamon 4.2+, -// after these applets have been updated on Spices. -var knownCinnamon4Conflicts = [ - // Applets - 'turbonote@iksws.com.b', - 'vnstat@linuxmint.com', - 'netusagemonitor@pdcurtis', - // Desklets - 'netusage@30yavash.com' -]; - var x11Only = [ "systray@cinnamon.org" ] @@ -96,6 +84,271 @@ function _createExtensionType(name, folder, manager, overrides){ */ var startTime; var extensions = []; + +// Track the currently loading extension for require() calls during module initialization +var currentlyLoadingExtension = null; +// UUIDs that have already been warned about using deprecated require() +var requireWarned = new Set(); + +// Stack-based module.exports compatibility for Node.js-style modules +var moduleStack = []; +// Cache of module.exports overrides, keyed by module path +var moduleExportsCache = {}; + +Object.defineProperty(globalThis, 'module', { + get: function() { + if (moduleStack.length > 0) { + return moduleStack[moduleStack.length - 1]; + } + return undefined; + }, + configurable: true +}); + +// Also provide 'exports' as a shorthand (some code uses it directly) +Object.defineProperty(globalThis, 'exports', { + get: function() { + if (moduleStack.length > 0) { + return moduleStack[moduleStack.length - 1].exports; + } + return undefined; + }, + configurable: true +}); + +/** + * getXletFromStack: + * + * Get the calling xlet by examining the stack trace. + * + * Returns: The Extension object if found, null otherwise + */ +function getXletFromStack() { + let stack = new Error().stack.split('\n'); + for (let i = 1; i < stack.length; i++) { + for (let folder of ['applets', 'desklets', 'extensions', 'search_providers']) { + let match = stack[i].match(new RegExp(`/${folder}/([^/]+)/`)); + if (match) { + return getExtension(match[1]) || getExtension(match[1].replace('!', '')); + } + } + } + return null; +} + +/** + * getCurrentExtension: + * + * Get the current xlet's Extension object. Can be called during module + * initialization or at runtime. + * + * Usage in xlets: + * const Extension = imports.ui.extension; + * const Me = Extension.getCurrentExtension(); + * const MyModule = Me.imports.myModule; + * + * Returns: The Extension object for the calling xlet + */ +function getCurrentExtension() { + return currentlyLoadingExtension || getXletFromStack(); +} + +/** + * xletRequire: + * @path (string): The module path to require + * + * ********************* DEPRECATED ************************ + * *** Use getCurrentExtension() to import local modules *** + * ********************************************************* + * + * Global require function for xlets. Supports: + * - Relative paths: './calendar' -> extension.imports.calendar + * - GI imports: 'gi.St' -> imports.gi.St + * - Cinnamon imports: 'ui.main' -> imports.ui.main + * + * Returns: The required module + */ +var _FunctionConstructor = (0).constructor.constructor; + +function _evalModule(extension, resolvedPath) { + let filePath = `${extension.meta.path}/${resolvedPath}.js`; + let file = Gio.File.new_for_path(filePath); + let [success, contents] = file.load_contents(null); + if (!success) { + throw new Error(`Failed to load ${filePath}`); + } + + let source = ByteArray.toString(contents); + let exports = {}; + let module = { exports: exports }; + + // Regex matches top level declarations and appends them to exports, + // mimicking how the native CJS importer handles var/function. + let re = /^(?:const|var|let|function|class)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm; + let match; + let exportLines = ''; + while ((match = re.exec(source)) !== null) { + exportLines += `if(typeof ${match[1]}!=='undefined')exports.${match[1]}=${match[1]};`; + } + + source = `'use strict';${source};${exportLines}return module.exports;//# sourceURL=${filePath}`; + + _FunctionConstructor( + 'require', 'exports', 'module', source + ).call( + exports, + function require(path) { return xletRequire(path); }, + exports, + module + ); + + return module.exports; +} + +function _requireLocal(extension, path) { + let parts = path.replace(/^\.\//, '').replace(/\.js$/, '').split('/'); + let resolvedPath = parts.filter(p => p !== '..').join('/'); + let cacheKey = `${extension.meta.path}/${resolvedPath}`; + + if (cacheKey in moduleExportsCache) { + return moduleExportsCache[cacheKey]; + } + + let moduleObj = { exports: {} }; + moduleObj._originalExports = moduleObj.exports; + moduleStack.push(moduleObj); + + let nativeModule; + try { + nativeModule = extension.imports; + for (let part of parts) { + if (part === '..') continue; + nativeModule = nativeModule[part]; + } + } finally { + moduleStack.pop(); + } + + let result; + + let exportsReplaced = moduleObj.exports !== moduleObj._originalExports; + let exportsMutated = Object.getOwnPropertyNames(moduleObj._originalExports).length > 0; + + if (exportsReplaced) { + result = moduleObj.exports; + } else if (exportsMutated) { + result = moduleObj._originalExports; + } else { + // CJS only exports var/function declarations as module properties. + // let/const/class values are inaccessible via the native module object. + // Fall back to evaluating the source with module/exports in scope. + result = _evalModule(extension, resolvedPath); + } + + moduleExportsCache[cacheKey] = result; + return result; +} + +function xletRequire(path) { + let extension = currentlyLoadingExtension || getXletFromStack(); + if (!extension) { + throw new Error(`require() called outside of xlet context: ${path}`); + } + + if (!requireWarned.has(extension.uuid)) { + requireWarned.add(extension.uuid); + global.logWarning(`${extension.uuid}: require() and module.exports are deprecated. Define exportable symbols with 'var' and use Extension.getCurrentExtension().imports to access local modules.`); + } + + // Relative paths: './foo' or '../foo' -> extension local module + if (path.startsWith('./') || path.startsWith('../')) { + return _requireLocal(extension, path); + } + + // GI imports: 'gi.St' -> imports.gi.St + if (path.startsWith('gi.')) { + return imports.gi[path.slice(3)]; + } + + // Cinnamon imports: 'ui.main', 'misc.util', etc. + let prefixes = ['ui', 'misc', 'perf']; + for (let prefix of prefixes) { + if (path.startsWith(prefix + '.')) { + return imports[prefix][path.slice(prefix.length + 1)]; + } + } + + // Bare name: try as a local module first, fall back to global + try { + return _requireLocal(extension, path); + } catch (e) { + return imports[path]; + } +} + +/** + * installXletImporter: + * @extension (Extension): The extension object + * + * Install native importer for xlet by temporarily modifying + * the search path. + */ +function installXletImporter(extension) { + // extension.dir is the actual directory containing the JS files, + // which might be a versioned subdirectory (e.g., .../uuid/6.0/) + // or the uuid directory itself for non-versioned xlets. + let parentPath = extension.dir.get_parent().get_path(); + let dirName = extension.dir.get_basename(); + + let oldSearchPath = imports.searchPath.slice(); + imports.searchPath = [parentPath]; + + try { + extension.imports = imports[dirName]; + } catch (e) { + imports.searchPath = oldSearchPath; + throw new Error(`Failed to create importer for ${extension.uuid} at ${parentPath}/${dirName}: ${e.message}`); + } + + imports.searchPath = oldSearchPath; + + if (!extension.imports) { + throw new Error(`Importer is null for ${extension.uuid} at ${parentPath}/${dirName}`); + } +} + +/** + * clearXletImportCache: + * @extension (Extension): The extension object + * + * Clear import cache to allow reloading of the xlet. + * Clears all cached module properties from the xlet's sub-importer. + */ +function clearXletImportCache(extension) { + if (!extension) return; + + // Clear all cached modules from the xlet's importer + if (!extension.imports) return; + + // Meta properties that should not be cleared + const metaProps = ['searchPath', '__moduleName__', '__parentModule__', + '__modulePath__', '__file__', '__init__', 'toString', + 'clearCache']; + + try { + let props = Object.getOwnPropertyNames(extension.imports); + for (let prop of props) { + if (!metaProps.includes(prop)) { + extension.imports.clearCache(prop); + } + } + } catch (e) { + // clearCache may not be available if cjs is not updated + } +} + +globalThis.require = xletRequire; + var Type = { EXTENSION: _createExtensionType("Extension", "extensions", ExtensionSystem, { requiredFunctions: ["init", "disable", "enable"], @@ -146,17 +399,11 @@ function logError(message, uuid, error, state) { } if (state !== State.X11_ONLY) { - error.stack = error.stack.split('\n') - .filter(function(line) { - return !line.match(/|wrapPromise/); - }) - .join('\n'); - global.logError(error); } else { global.logWarning(error.message); } - + // An error during initialization leads to unloading the extension again. let extension = getExtension(uuid); if (extension) { @@ -182,25 +429,25 @@ function ensureFileExists(file) { // The Extension object itself function Extension(type, uuid) { let extension = getExtension(uuid); - if (extension) { - return Promise.resolve(true); - } + if (extension) return Promise.resolve(true); + let force = false; if (uuid.substr(0, 1) === '!') { uuid = uuid.replace(/^!/, ''); force = true; } - let dir = findExtensionDirectory(uuid, type.userDir, type.folder); + let dir = findExtensionDirectory(uuid, type.userDir, type.folder); if (dir == null) { forgetExtension(uuid, type, true); return Promise.resolve(null); } + return this._init(dir, type, uuid, force); } Extension.prototype = { - _init: function(dir, type, uuid, force) { + _init: async function(dir, type, uuid, force) { this.name = type.name; this.uuid = uuid; this.dir = dir; @@ -211,10 +458,34 @@ Extension.prototype = { this.iconDirectory = null; this.meta = createMetaDummy(uuid, dir.get_path(), State.INITIALIZING); - let isPotentialNMClientConflict = knownCinnamon4Conflicts.indexOf(uuid) > -1; + try { + this.meta = await loadMetaData({ + state: this.meta.state, + path: this.meta.path, + uuid: uuid, + userDir: type.userDir, + folder: type.folder, + force: force + }); + + // Timer needs to start after the first initial I/O + startTime = new Date().getTime(); + + if (!force) { + this.validateMetaData(); + } + + this.dir = await findExtensionSubdirectory(this.dir); + this.meta.path = this.dir.get_path(); + + this._finishLoad(type, uuid); + } catch (e) { + this._handleLoadError(type, uuid, e); + } + }, - const finishLoad = () => { - // Many xlets still use appletMeta/deskletMeta to get the path + _finishLoad: function(type, uuid) { + try { type.legacyMeta[uuid] = {path: this.meta.path}; ensureFileExists(this.dir.get_child(`${this.lowerType}.js`)); @@ -226,85 +497,52 @@ Extension.prototype = { }); } this.loadIconDirectory(this.dir); - // get [extension/applet/desklet].js - return requireModule( - `${this.meta.path}/${this.lowerType}.js`, // path - this.meta.path, // dir, - this.meta, // meta - this.lowerType, // type - true, // async - true // returnIndex - ); - }; - - return loadMetaData({ - state: this.meta.state, - path: this.meta.path, - uuid: uuid, - userDir: type.userDir, - folder: type.folder, - force: force - }).then((meta) => { - // Timer needs to start after the first initial I/O, otherwise every applet shows as taking 1-2 seconds to load. - // Maybe because of how promises are wired up in CJS? - // https://github.com/linuxmint/cjs/blob/055da399c794b0b4d76ecd7b5fabf7f960f77518/modules/_lie.js#L9 - startTime = new Date().getTime(); - this.meta = meta; - - if (!force) { - this.validateMetaData(); - } - return findExtensionSubdirectory(this.dir).then((dir) => { - this.dir = dir; - this.meta.path = this.dir.get_path(); + installXletImporter(this); + currentlyLoadingExtension = this; - // If an xlet has known usage of imports.gi.NMClient, we require them to have a - // 4.0 directory. It is the only way to assume they are patched for Cinnamon 4 from here. - if (isPotentialNMClientConflict && this.meta.path.indexOf(`/4.0`) === -1) { - throw new Error(`Found unpatched usage of imports.gi.NMClient for ${this.lowerType} ${uuid}`); - } + try { + this.module = this.imports[this.lowerType]; + } finally { + currentlyLoadingExtension = null; + } - return finishLoad(); - }); - }).then((moduleIndex) => { - if (moduleIndex == null) { - throw new Error(`Could not find module index: ${moduleIndex}`); + if (this.module == null) { + throw new Error(`Could not load module for ${uuid}`); } - this.moduleIndex = moduleIndex; - for (let i = 0; i < type.requiredFunctions.length; i++) { - let func = type.requiredFunctions[i]; - if (!getModuleByIndex(moduleIndex)[func]) { + + for (let func of type.requiredFunctions) { + if (!this.module[func]) { throw new Error(`Function "${func}" is missing`); } } - // Add the extension to the global collection extensions.push(this); - if(!type.callbacks.finishExtensionLoad(extensions.length - 1)) { - throw new Error(`${type.name} ${uuid}: Could not create ${this.lowerType} object.`); + if (!type.callbacks.finishExtensionLoad(extensions.length - 1)) { + throw new Error(`Could not create ${this.lowerType} object.`); } + this.finalize(); Main.cinnamonDBusService.EmitXletAddedComplete(true, uuid); - }).catch((e) => { - // Silently fail to load xlets that aren't actually installed - - // but no error, since the user can't do anything about it anyhow - // (short of editing gsettings). Silent failure is consistent with - // other reactions in Cinnamon to missing items (e.g. panel launchers - // just don't show up if their program isn't installed, but we don't - // remove them or anything) - Main.cinnamonDBusService.EmitXletAddedComplete(false, uuid); - - if (e.cause == null || e.cause !== State.X11_ONLY) { - Main.xlet_startup_error = true; - } - forgetExtension(uuid, type); - if (e._alreadyLogged) { - return; - } - logError(`Error importing ${this.lowerType}.js from ${uuid}`, uuid, e); - }); + + } catch (e) { + this._handleLoadError(type, uuid, e); + } + }, + + _handleLoadError: function(type, uuid, error) { + Main.cinnamonDBusService.EmitXletAddedComplete(false, uuid); + + if (error.cause == null || error.cause !== State.X11_ONLY) { + Main.xlet_startup_error = true; + } + + forgetExtension(uuid, type); + + if (!error._alreadyLogged) { + logError(`Error importing ${this.lowerType}.js from ${uuid}`, uuid, error); + } }, finalize: function() { @@ -579,10 +817,23 @@ function unloadExtension(uuid, type, deleteConfig = true, reload = false) { function forgetExtension(extensionIndex, uuid, type, forgetMeta) { if (typeof extensions[extensionIndex] !== 'undefined') { - unloadModule(extensions[extensionIndex].moduleIndex); - try { - delete imports[type.folder][uuid]; - } catch (e) {} + let extension = extensions[extensionIndex]; + + // Clear module.exports cache entries for this extension + let pathPrefix = extension.meta.path + '/'; + for (let key in moduleExportsCache) { + if (key.startsWith(pathPrefix)) { + delete moduleExportsCache[key]; + } + } + + // Clear the import cache to allow reloading (must be done before nulling references) + clearXletImportCache(extension); + + // Clear the module reference + extension.module = null; + extension.imports = null; + if (forgetMeta) { extensions[extensionIndex] = undefined; extensions.splice(extensionIndex, 1); @@ -661,29 +912,28 @@ function maybeAddWindowAttentionHandlerRole(meta) { } function loadMetaData({state, path, uuid, userDir, folder, force}) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { let dir = findExtensionDirectory(uuid, userDir, folder); let meta; let metadataFile = dir.get_child('metadata.json'); let oldState = state ? state : State.INITIALIZING; let oldPath = path ? path : dir.get_path(); + ensureFileExists(metadataFile); + metadataFile.load_contents_async(null, (object, result) => { try { let [success, json] = metadataFile.load_contents_finish(result); if (!success) { - reject(); - return; + throw new Error('Failed to load metadata'); } meta = JSON.parse(ByteArray.toString(json)); - maybeAddWindowAttentionHandlerRole(meta); } catch (e) { logError(`Failed to load/parse metadata.json`, uuid, e); meta = createMetaDummy(uuid, oldPath, State.ERROR); - } - // Store some additional crap here + meta.state = oldState; meta.path = oldPath; meta.error = ''; @@ -702,45 +952,47 @@ function loadMetaData({state, path, uuid, userDir, folder, force}) { * equal to the current running version. If no such version is found, the * original directory is returned. * - * Returns (Gio.File): directory object of the desired directory. + * Returns: Promise that resolves to the directory */ function findExtensionSubdirectory(dir) { - return new Promise(function(resolve, reject) { + return new Promise((resolve) => { dir.enumerate_children_async( 'standard::*', Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, - function(obj, res) { - try { - let fileEnum = obj.enumerate_children_finish(res); - let info; - let largest = null; - while ((info = fileEnum.next_file(null)) != null) { - let fileType = info.get_file_type(); - if (fileType !== Gio.FileType.DIRECTORY) { - continue; - } - - let name = info.get_name(); - if (!name.match(/^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$/)) { - continue; + (obj, res) => { + try { + let fileEnum = obj.enumerate_children_finish(res); + let info; + let largest = null; + + while ((info = fileEnum.next_file(null)) != null) { + let fileType = info.get_file_type(); + if (fileType !== Gio.FileType.DIRECTORY) { + continue; + } + + let name = info.get_name(); + if (!name.match(/^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$/)) { + continue; + } + + if (versionLeq(name, Config.PACKAGE_VERSION) && + (!largest || versionLeq(largest[0], name))) { + largest = [name, fileEnum.get_child(info)]; + } } - if (versionLeq(name, Config.PACKAGE_VERSION) && - (!largest || versionLeq(largest[0], name))) { - largest = [name, fileEnum.get_child(info)]; - } + fileEnum.close(null); + resolve(largest ? largest[1] : dir); + } catch (e) { + logError(`Error looking for extension version for ${dir.get_basename()}`, + 'findExtensionSubdirectory', e); + resolve(dir); // Fall back to original dir } - - fileEnum.close(null); - resolve(largest ? largest[1] : dir); - } catch (e) { - logError(`Error looking for extension version for ${dir.get_basename()} in directory ${dir}`, 'findExtensionSubdirectory', e); - resolve(dir) } - - }); + ); }); } diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 35bb54a813..27c6f3ae1a 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -2,7 +2,6 @@ const Main = imports.ui.main; const Extension = imports.ui.extension; -const {getModuleByIndex} = imports.misc.fileUtils; // Maps uuid -> importer object (extension directory tree) var extensions; @@ -33,7 +32,7 @@ function enableExtension(uuid) { // Callback for extension.js function prepareExtensionUnload(extension) { try { - getModuleByIndex(extension.moduleIndex).disable(); + extension.module.disable(); } catch (e) { Extension.logError('Failed to evaluate \'disable\' function on extension: ' + extension.uuid, e); } @@ -50,7 +49,7 @@ function prepareExtensionUnload(extension) { // Callback for extension.js function prepareExtensionReload(extension) { try { - let on_extension_reloaded = getModuleByIndex(extension.moduleIndex).on_extension_reloaded; + let on_extension_reloaded = extension.module.on_extension_reloaded; if (on_extension_reloaded) on_extension_reloaded(); } catch (e) { Extension.logError('Failed to evaluate \'on_extension_reloaded\' function on extension: ' + extension.uuid, e); @@ -60,12 +59,12 @@ function prepareExtensionReload(extension) { // Callback for extension.js function finishExtensionLoad(extensionIndex) { let extension = Extension.extensions[extensionIndex]; - if (!extension.lockRole(getModuleByIndex(extension.moduleIndex))) { + if (!extension.lockRole(extension.module)) { return false; } try { - getModuleByIndex(extension.moduleIndex).init(extension.meta); + extension.module.init(extension.meta); } catch (e) { Extension.logError('Failed to evaluate \'init\' function on extension: ' + extension.uuid, e); return false; @@ -73,7 +72,7 @@ function finishExtensionLoad(extensionIndex) { let extensionCallbacks; try { - extensionCallbacks = getModuleByIndex(extension.moduleIndex).enable(); + extensionCallbacks = extension.module.enable(); } catch (e) { Extension.logError('Failed to evaluate \'enable\' function on extension: ' + extension.uuid, e); return false; diff --git a/js/ui/main.js b/js/ui/main.js index cdadc41a90..62bd6dab4a 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -306,6 +306,12 @@ function start() { global.logError = _logError; global.log = _logInfo; + try { + imports.clearCache; + } catch (e) { + global.logWarning('CJS clearCache not available. Xlet reloading may not work correctly (cjs update required).'); + } + let cinnamonStartTime = new Date().getTime(); log(`About to start Cinnamon (${Meta.is_wayland_compositor() ? "Wayland" : "X11"} backend)`); diff --git a/js/ui/searchProviderManager.js b/js/ui/searchProviderManager.js index 18651dfc59..acb1a14c05 100644 --- a/js/ui/searchProviderManager.js +++ b/js/ui/searchProviderManager.js @@ -1,7 +1,6 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Extension = imports.ui.extension; -const {getModuleByIndex} = imports.misc.fileUtils; const GLib = imports.gi.GLib; // Maps uuid -> importer object (extension directory tree) @@ -23,7 +22,7 @@ function prepareExtensionUnload(extension) { // Callback for extension.js function finishExtensionLoad(extensionIndex) { let extension = Extension.extensions[extensionIndex]; - searchProviderObj[extension.uuid] = getModuleByIndex(extension.moduleIndex); + searchProviderObj[extension.uuid] = extension.module; return true; }