mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-27 03:57:21 +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:
parent
b44dc39299
commit
1c23059204
55
boot/boot.js
55
boot/boot.js
@ -1237,15 +1237,39 @@ $tw.Wiki = function(options) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// Read plugin info for all plugins
|
||||
this.readPluginInfo = function() {
|
||||
for(var title in tiddlers) {
|
||||
var tiddler = tiddlers[title];
|
||||
if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type")) {
|
||||
pluginInfo[tiddler.fields.title] = JSON.parse(tiddler.fields.text);
|
||||
// Get an array of all the currently recognised plugin types
|
||||
this.getPluginTypes = function() {
|
||||
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];
|
||||
if(tiddler) {
|
||||
if(tiddler.fields.type === "application/json" && tiddler.hasField("plugin-type")) {
|
||||
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
|
||||
@ -1253,14 +1277,15 @@ $tw.Wiki = function(options) {
|
||||
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) {
|
||||
var self = this,
|
||||
registeredTitles = [],
|
||||
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);
|
||||
if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") {
|
||||
self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered
|
||||
pluginTiddlers.push(tiddler);
|
||||
registeredTitles.push(tiddler.fields.title);
|
||||
}
|
||||
@ -1278,19 +1303,19 @@ $tw.Wiki = function(options) {
|
||||
return registeredTitles;
|
||||
};
|
||||
|
||||
// Unregister the plugin tiddlers of a particular type, returning an array of the titles affected
|
||||
this.unregisterPluginTiddlers = function(pluginType) {
|
||||
// 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,titles) {
|
||||
var self = this,
|
||||
titles = [];
|
||||
unregisteredTitles = [];
|
||||
// Remove any previous registered plugins of this type
|
||||
for(var t=pluginTiddlers.length-1; t>=0; t--) {
|
||||
var tiddler = pluginTiddlers[t];
|
||||
if(tiddler.fields["plugin-type"] === pluginType) {
|
||||
titles.push(tiddler.fields.title);
|
||||
if(tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType) && (!titles || titles.indexOf(tiddler.fields.title) !== -1)) {
|
||||
unregisteredTitles.push(tiddler.fields.title);
|
||||
pluginTiddlers.splice(t,1);
|
||||
}
|
||||
}
|
||||
return titles;
|
||||
return unregisteredTitles;
|
||||
};
|
||||
|
||||
// Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers
|
||||
|
@ -78,6 +78,7 @@ Plugins/NoInfoFound/Hint: No ''"<$text text=<<currentTab>>/>"'' found
|
||||
Plugins/NotInstalled/Hint: This plugin is not currently installed
|
||||
Plugins/OpenPluginLibrary: open plugin library
|
||||
Plugins/ClosePluginLibrary: close plugin library
|
||||
Plugins/PluginWillRequireReload: (requires reload)
|
||||
Plugins/Plugins/Caption: Plugins
|
||||
Plugins/Plugins/Hint: Plugins
|
||||
Plugins/Reinstall/Caption: reinstall
|
||||
|
@ -59,7 +59,7 @@ MissingTiddler/Hint: Missing tiddler "<$text text=<<currentTiddler>>/>" -- click
|
||||
No: No
|
||||
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.
|
||||
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
|
||||
SystemTiddler/Tooltip: This is a system tiddler
|
||||
SystemTiddlers/Include/Prompt: Include system tiddlers
|
||||
|
@ -59,7 +59,7 @@ Command.prototype.execute = function() {
|
||||
title: upgradeLibraryTitle,
|
||||
type: "application/json",
|
||||
"plugin-type": "library",
|
||||
"text": JSON.stringify({tiddlers: tiddlers},null,$tw.config.preferences.jsonSpaces)
|
||||
"text": JSON.stringify({tiddlers: tiddlers})
|
||||
};
|
||||
wiki.addTiddler(new $tw.Tiddler(pluginFields));
|
||||
return null;
|
||||
|
@ -65,10 +65,11 @@ Command.prototype.execute = function() {
|
||||
// Save each JSON file and collect the skinny data
|
||||
var pathname = path.resolve(self.commander.outputPath,basepath + encodeURIComponent(title) + ".json");
|
||||
$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
|
||||
var pluginTiddlers = JSON.parse(tiddler.text),
|
||||
readmeContent = (pluginTiddlers.tiddlers[title + "/readme"] || {}).text,
|
||||
doesContainJavaScript = !!$tw.wiki.doesPluginInfoContainModules(pluginTiddlers),
|
||||
iconTiddler = pluginTiddlers.tiddlers[title + "/icon"] || {},
|
||||
iconType = iconTiddler.type,
|
||||
iconText = iconTiddler.text,
|
||||
@ -76,7 +77,12 @@ Command.prototype.execute = function() {
|
||||
if(iconType && iconText) {
|
||||
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
|
||||
if(skinnyListTitle) {
|
||||
|
@ -18,6 +18,8 @@ exports.before = ["startup"];
|
||||
exports.after = ["load-modules"];
|
||||
exports.synchronous = true;
|
||||
|
||||
var TITLE_INFO_PLUGIN = "$:/temp/info-plugin";
|
||||
|
||||
exports.startup = function() {
|
||||
// Collect up the info tiddlers
|
||||
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 = {
|
||||
title: "$:/temp/info-plugin",
|
||||
title: TITLE_INFO_PLUGIN,
|
||||
type: "application/json",
|
||||
"plugin-type": "info",
|
||||
text: JSON.stringify({tiddlers: infoTiddlerFields},null,$tw.config.preferences.jsonSpaces)
|
||||
};
|
||||
$tw.wiki.addTiddler(new $tw.Tiddler(fields));
|
||||
$tw.wiki.readPluginInfo();
|
||||
$tw.wiki.readPluginInfo([TITLE_INFO_PLUGIN]);
|
||||
$tw.wiki.registerPluginTiddlers("info");
|
||||
$tw.wiki.unpackPluginTiddlers();
|
||||
};
|
||||
|
59
core/modules/startup/plugins.js
Normal file
59
core/modules/startup/plugins.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
})();
|
@ -1450,5 +1450,25 @@ exports.invokeUpgraders = function(titles,tiddlers) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
@ -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}}""">
|
||||
{{$:/language/ControlPanel/Plugins/Reinstall/Caption}}
|
||||
</$list>
|
||||
<$reveal stateTitle=<<assetInfo>> stateField="contains-javascript" type="match" text="yes">{{$:/language/ControlPanel/Plugins/PluginWillRequireReload}}</$reveal>
|
||||
</$button>
|
||||
\end
|
||||
|
||||
@ -35,7 +36,7 @@ $:/state/add-plugin-info/$(connectionTiddler)$/$(assetInfo)$
|
||||
</$list>
|
||||
</div>
|
||||
<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>
|
||||
<div><em><$view tiddler=<<assetInfo>> field="version"/></em></div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ tags: $:/tags/PageTemplate
|
||||
|
||||
\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">
|
||||
|
||||
|
7
core/wiki/config/RegisterPluginTypes.multids
Normal file
7
core/wiki/config/RegisterPluginTypes.multids
Normal file
@ -0,0 +1,7 @@
|
||||
title: $:/config/RegisterPluginType/
|
||||
|
||||
plugin: yes
|
||||
theme: yes
|
||||
language: yes
|
||||
info: no
|
||||
import: no
|
Loading…
Reference in New Issue
Block a user