1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-23 10:07:19 +00:00

Dynamic loading/unloading of plugins (#4259)

* First pass at dynamic loading/unloading

* Show warning for changes to plugins containing JS modules

* Use $:/config/RegisterPluginType/* for configuring whether a plugin type is automatically registered

Where "registered" means "the constituent shadows are loaded".

* Fix the info plugin

The previous mechanism re-read all plugin info during startup

* Don't prettify JSON in the plugin library

* Indicate in plugin library whether a plugin requires reloading

* Display the highlighted plugin name in the plugin chooser

And if there's no name field fall back to the part of the title after the final slash.
This commit is contained in:
Jeremy Ruston 2019-09-16 12:15:39 +01:00 committed by GitHub
parent b44dc39299
commit 1c23059204
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 145 additions and 24 deletions

View File

@ -1237,15 +1237,39 @@ $tw.Wiki = function(options) {
return null; return null;
}; };
// Read plugin info for all plugins // Get an array of all the currently recognised plugin types
this.readPluginInfo = function() { this.getPluginTypes = function() {
for(var title in tiddlers) { var types = [];
$tw.utils.each(pluginTiddlers,function(pluginTiddler) {
var pluginType = pluginTiddler.fields["plugin-type"];
if(pluginType && types.indexOf(pluginType) === -1) {
types.push(pluginType);
}
});
return types;
};
// Read plugin info for all plugins, or just an array of titles. Returns the number of plugins updated or deleted
this.readPluginInfo = function(titles) {
var results = {
modifiedPlugins: [],
deletedPlugins: []
};
$tw.utils.each(titles || getTiddlerTitles(),function(title) {
var tiddler = tiddlers[title]; var tiddler = tiddlers[title];
if(tiddler) {
if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type")) { if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type")) {
pluginInfo[tiddler.fields.title] = JSON.parse(tiddler.fields.text); pluginInfo[tiddler.fields.title] = JSON.parse(tiddler.fields.text);
results.modifiedPlugins.push(tiddler.fields.title);
} }
} else {
if(pluginInfo[title]) {
delete pluginInfo[title];
results.deletedPlugins.push(title);
} }
}
});
return results;
}; };
// Get plugin info for a plugin // Get plugin info for a plugin
@ -1253,14 +1277,15 @@ $tw.Wiki = function(options) {
return pluginInfo[title]; return pluginInfo[title];
}; };
// Register the plugin tiddlers of a particular type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected // Register the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting registration to an array of tiddler titles. Return the array of titles affected
this.registerPluginTiddlers = function(pluginType,titles) { this.registerPluginTiddlers = function(pluginType,titles) {
var self = this, var self = this,
registeredTitles = [], registeredTitles = [],
checkTiddler = function(tiddler,title) { checkTiddler = function(tiddler,title) {
if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] === pluginType) { if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) {
var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title); var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title);
if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") { if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") {
self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered
pluginTiddlers.push(tiddler); pluginTiddlers.push(tiddler);
registeredTitles.push(tiddler.fields.title); registeredTitles.push(tiddler.fields.title);
} }
@ -1278,19 +1303,19 @@ $tw.Wiki = function(options) {
return registeredTitles; return registeredTitles;
}; };
// Unregister the plugin tiddlers of a particular type, returning an array of the titles affected // Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected
this.unregisterPluginTiddlers = function(pluginType) { this.unregisterPluginTiddlers = function(pluginType,titles) {
var self = this, var self = this,
titles = []; unregisteredTitles = [];
// Remove any previous registered plugins of this type // Remove any previous registered plugins of this type
for(var t=pluginTiddlers.length-1; t>=0; t--) { for(var t=pluginTiddlers.length-1; t>=0; t--) {
var tiddler = pluginTiddlers[t]; var tiddler = pluginTiddlers[t];
if(tiddler.fields["plugin-type"] === pluginType) { if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) {
titles.push(tiddler.fields.title); unregisteredTitles.push(tiddler.fields.title);
pluginTiddlers.splice(t,1); pluginTiddlers.splice(t,1);
} }
} }
return titles; return unregisteredTitles;
}; };
// Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers // Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers

View File

@ -78,6 +78,7 @@ Plugins/NoInfoFound/Hint: No ''"<$text text=<<currentTab>>/>"'' found
Plugins/NotInstalled/Hint: This plugin is not currently installed Plugins/NotInstalled/Hint: This plugin is not currently installed
Plugins/OpenPluginLibrary: open plugin library Plugins/OpenPluginLibrary: open plugin library
Plugins/ClosePluginLibrary: close plugin library Plugins/ClosePluginLibrary: close plugin library
Plugins/PluginWillRequireReload: (requires reload)
Plugins/Plugins/Caption: Plugins Plugins/Plugins/Caption: Plugins
Plugins/Plugins/Hint: Plugins Plugins/Plugins/Hint: Plugins
Plugins/Reinstall/Caption: reinstall Plugins/Reinstall/Caption: reinstall

View File

@ -59,7 +59,7 @@ MissingTiddler/Hint: Missing tiddler "<$text text=<<currentTiddler>>/>" -- click
No: No No: No
OfficialPluginLibrary: Official ~TiddlyWiki Plugin Library OfficialPluginLibrary: Official ~TiddlyWiki Plugin Library
OfficialPluginLibrary/Hint: The official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team. OfficialPluginLibrary/Hint: The official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
PluginReloadWarning: Please save {{$:/core/ui/Buttons/save-wiki}} and reload {{$:/core/ui/Buttons/refresh}} to allow changes to plugins to take effect PluginReloadWarning: Please save {{$:/core/ui/Buttons/save-wiki}} and reload {{$:/core/ui/Buttons/refresh}} to allow changes to ~JavaScript plugins to take effect
RecentChanges/DateFormat: DDth MMM YYYY RecentChanges/DateFormat: DDth MMM YYYY
SystemTiddler/Tooltip: This is a system tiddler SystemTiddler/Tooltip: This is a system tiddler
SystemTiddlers/Include/Prompt: Include system tiddlers SystemTiddlers/Include/Prompt: Include system tiddlers

View File

@ -59,7 +59,7 @@ Command.prototype.execute = function() {
title: upgradeLibraryTitle, title: upgradeLibraryTitle,
type: "application/json", type: "application/json",
"plugin-type": "library", "plugin-type": "library",
"text": JSON.stringify({tiddlers: tiddlers},null,$tw.config.preferences.jsonSpaces) "text": JSON.stringify({tiddlers: tiddlers})
}; };
wiki.addTiddler(new $tw.Tiddler(pluginFields)); wiki.addTiddler(new $tw.Tiddler(pluginFields));
return null; return null;

View File

@ -65,10 +65,11 @@ Command.prototype.execute = function() {
// Save each JSON file and collect the skinny data // Save each JSON file and collect the skinny data
var pathname = path.resolve(self.commander.outputPath,basepath + encodeURIComponent(title) + ".json"); var pathname = path.resolve(self.commander.outputPath,basepath + encodeURIComponent(title) + ".json");
$tw.utils.createFileDirectories(pathname); $tw.utils.createFileDirectories(pathname);
fs.writeFileSync(pathname,JSON.stringify(tiddler,null,$tw.config.preferences.jsonSpaces),"utf8"); fs.writeFileSync(pathname,JSON.stringify(tiddler),"utf8");
// Collect the skinny list data // Collect the skinny list data
var pluginTiddlers = JSON.parse(tiddler.text), var pluginTiddlers = JSON.parse(tiddler.text),
readmeContent = (pluginTiddlers.tiddlers[title + "/readme"] || {}).text, readmeContent = (pluginTiddlers.tiddlers[title + "/readme"] || {}).text,
doesContainJavaScript = !!$tw.wiki.doesPluginInfoContainModules(pluginTiddlers),
iconTiddler = pluginTiddlers.tiddlers[title + "/icon"] || {}, iconTiddler = pluginTiddlers.tiddlers[title + "/icon"] || {},
iconType = iconTiddler.type, iconType = iconTiddler.type,
iconText = iconTiddler.text, iconText = iconTiddler.text,
@ -76,7 +77,12 @@ Command.prototype.execute = function() {
if(iconType && iconText) { if(iconType && iconText) {
iconContent = $tw.utils.makeDataUri(iconText,iconType); iconContent = $tw.utils.makeDataUri(iconText,iconType);
} }
skinnyList.push($tw.utils.extend({},tiddler,{text: undefined, readme: readmeContent, icon: iconContent})); skinnyList.push($tw.utils.extend({},tiddler,{
text: undefined,
readme: readmeContent,
"contains-javascript": doesContainJavaScript ? "yes" : "no",
icon: iconContent
}));
}); });
// Save the catalogue tiddler // Save the catalogue tiddler
if(skinnyListTitle) { if(skinnyListTitle) {

View File

@ -18,6 +18,8 @@ exports.before = ["startup"];
exports.after = ["load-modules"]; exports.after = ["load-modules"];
exports.synchronous = true; exports.synchronous = true;
var TITLE_INFO_PLUGIN = "$:/temp/info-plugin";
exports.startup = function() { exports.startup = function() {
// Collect up the info tiddlers // Collect up the info tiddlers
var infoTiddlerFields = {}; var infoTiddlerFields = {};
@ -32,15 +34,15 @@ exports.startup = function() {
}); });
} }
}); });
// Bake the info tiddlers into a plugin // Bake the info tiddlers into a plugin. We use the non-standard plugin-type "info" because ordinary plugins are only registered asynchronously after being loaded dynamically
var fields = { var fields = {
title: "$:/temp/info-plugin", title: TITLE_INFO_PLUGIN,
type: "application/json", type: "application/json",
"plugin-type": "info", "plugin-type": "info",
text: JSON.stringify({tiddlers: infoTiddlerFields},null,$tw.config.preferences.jsonSpaces) text: JSON.stringify({tiddlers: infoTiddlerFields},null,$tw.config.preferences.jsonSpaces)
}; };
$tw.wiki.addTiddler(new $tw.Tiddler(fields)); $tw.wiki.addTiddler(new $tw.Tiddler(fields));
$tw.wiki.readPluginInfo(); $tw.wiki.readPluginInfo([TITLE_INFO_PLUGIN]);
$tw.wiki.registerPluginTiddlers("info"); $tw.wiki.registerPluginTiddlers("info");
$tw.wiki.unpackPluginTiddlers(); $tw.wiki.unpackPluginTiddlers();
}; };

View File

@ -0,0 +1,59 @@
/*\
title: $:/core/modules/startup/plugins.js
type: application/javascript
module-type: startup
Startup logic concerned with managing plugins
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Export name and synchronous status
exports.name = "plugins";
exports.after = ["load-modules"];
exports.synchronous = true;
var TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE = "$:/status/RequireReloadDueToPluginChange";
var PREFIX_CONFIG_REGISTER_PLUGIN_TYPE = "$:/config/RegisterPluginType/";
exports.startup = function() {
$tw.wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "no"});
$tw.wiki.addEventListener("change",function(changes) {
var changesToProcess = [],
requireReloadDueToPluginChange = false;
$tw.utils.each(Object.keys(changes),function(title) {
var tiddler = $tw.wiki.getTiddler(title),
containsModules = $tw.wiki.doesPluginContainModules(title);
if(containsModules) {
requireReloadDueToPluginChange = true;
} else if(tiddler) {
var pluginType = tiddler.fields["plugin-type"];
if($tw.wiki.getTiddlerText(PREFIX_CONFIG_REGISTER_PLUGIN_TYPE + (tiddler.fields["plugin-type"] || ""),"no") === "yes") {
changesToProcess.push(title);
}
}
});
if(requireReloadDueToPluginChange) {
$tw.wiki.addTiddler({title: TITLE_REQUIRE_RELOAD_DUE_TO_PLUGIN_CHANGE,text: "yes"});
}
// Read or delete the plugin info of the changed tiddlers
if(changesToProcess.length > 0) {
var changes = $tw.wiki.readPluginInfo(changesToProcess);
if(changes.modifiedPlugins.length > 0 || changes.deletedPlugins.length > 0) {
// (Re-)register any modified plugins
$tw.wiki.registerPluginTiddlers(null,changes.modifiedPlugins);
// Unregister any deleted plugins
$tw.wiki.unregisterPluginTiddlers(null,changes.deletedPlugins);
// Unpack the shadow tiddlers
$tw.wiki.unpackPluginTiddlers();
}
}
});
};
})();

View File

@ -1450,5 +1450,25 @@ exports.invokeUpgraders = function(titles,tiddlers) {
return messages; return messages;
}; };
// Determine whether a plugin by title contains JS modules.
exports.doesPluginContainModules = function(title) {
return this.doesPluginInfoContainModules(this.getPluginInfo(title) || this.getTiddlerDataCached(title));
};
// Determine whether a plugin info structure contains JS modules.
exports.doesPluginInfoContainModules = function(pluginInfo) {
if(pluginInfo) {
var foundModule = false;
$tw.utils.each(pluginInfo.tiddlers,function(tiddler) {
if(tiddler.type === "application/javascript" && $tw.utils.hop(tiddler,"module-type")) {
foundModule = true;
}
});
return foundModule;
} else {
return null;
}
};
})(); })();

View File

@ -7,6 +7,7 @@ subtitle: {{$:/core/images/download-button}} {{$:/language/ControlPanel/Plugins/
<$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""{{$:/language/ControlPanel/Plugins/Install/Caption}}"""> <$list filter="[<assetInfo>get[original-title]get[version]]" variable="installedVersion" emptyMessage="""{{$:/language/ControlPanel/Plugins/Install/Caption}}""">
{{$:/language/ControlPanel/Plugins/Reinstall/Caption}} {{$:/language/ControlPanel/Plugins/Reinstall/Caption}}
</$list> </$list>
<$reveal stateTitle=<<assetInfo>> stateField="contains-javascript" type="match" text="yes">{{$:/language/ControlPanel/Plugins/PluginWillRequireReload}}</$reveal>
</$button> </$button>
\end \end
@ -35,7 +36,7 @@ $:/state/add-plugin-info/$(connectionTiddler)$/$(assetInfo)$
</$list> </$list>
</div> </div>
<div class="tc-plugin-info-chunk"> <div class="tc-plugin-info-chunk">
<h1><$view tiddler=<<assetInfo>> field="description"/></h1> <h1><strong><$text text={{{ [<assetInfo>get[name]] ~[<assetInfo>get[original-title]split[/]last[1]] }}}/></strong>: <$view tiddler=<<assetInfo>> field="description"/></h1>
<h2><$view tiddler=<<assetInfo>> field="original-title"/></h2> <h2><$view tiddler=<<assetInfo>> field="original-title"/></h2>
<div><em><$view tiddler=<<assetInfo>> field="version"/></em></div> <div><em><$view tiddler=<<assetInfo>> field="version"/></em></div>
</div> </div>

View File

@ -3,7 +3,7 @@ tags: $:/tags/PageTemplate
\define lingo-base() $:/language/ \define lingo-base() $:/language/
<$list filter="[has[plugin-type]haschanged[]!plugin-type[import]limit[1]]"> <$list filter="[{$:/status/RequireReloadDueToPluginChange}match[yes]]">
<$reveal type="nomatch" state="$:/temp/HidePluginWarning" text="yes"> <$reveal type="nomatch" state="$:/temp/HidePluginWarning" text="yes">

View File

@ -0,0 +1,7 @@
title: $:/config/RegisterPluginType/
plugin: yes
theme: yes
language: yes
info: no
import: no