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.
fix-syncer
Jeremy Ruston 2019-09-16 12:15:39 +01:00 zatwierdzone przez GitHub
rodzic b44dc39299
commit 1c23059204
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 145 dodań i 24 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();
};

Wyświetl plik

@ -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();
}
}
});
};
})();

Wyświetl plik

@ -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;
}
};
})();

Wyświetl plik

@ -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>

Wyświetl plik

@ -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">

Wyświetl plik

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