@lumino/application
Advanced tools
Comparing version 2.0.0-alpha.1 to 2.0.0-alpha.2
@@ -1,2 +0,2 @@ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@lumino/commands"),require("@lumino/coreutils"),require("@lumino/widgets")):"function"==typeof define&&define.amd?define(["exports","@lumino/commands","@lumino/coreutils","@lumino/widgets"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).lumino_application={},e.lumino_commands,e.lumino_coreutils,e.lumino_widgets)}(this,(function(e,t,i,r){"use strict";var n;!function(e){e.createPluginMap=function(){return Object.create(null)},e.createServiceMap=function(){return new Map},e.createPluginData=function(e){return{id:e.id,service:null,promise:null,activated:!1,activate:e.activate,provides:e.provides||null,autoStart:e.autoStart||!1,requires:e.requires?e.requires.slice():[],optional:e.optional?e.optional.slice():[]}},e.ensureNoCycle=function(e,t,i){let r=e.requires.concat(e.optional);if(!e.provides||0===r.length)return;let n=[e.id];if(r.some((function r(s){if(s===e.provides)return!0;let o=i.get(s);if(!o)return!1;let l=t[o],a=l.requires.concat(l.optional);if(0===a.length)return!1;if(n.push(o),a.some(r))return!0;return n.pop(),!1})))throw new Error(`Cycle detected: ${n.join(" -> ")}.`)},e.collectStartupPlugins=function(e,t){let i=Object.create(null);for(let t in e)e[t].autoStart&&(i[t]=!0);if(t.startPlugins)for(let e of t.startPlugins)i[e]=!0;if(t.ignorePlugins)for(let e of t.ignorePlugins)delete i[e];return Object.keys(i)}}(n||(n={})),e.Application=class{constructor(e){this._started=!1,this._pluginMap=n.createPluginMap(),this._serviceMap=n.createServiceMap(),this._delegate=new i.PromiseDelegate;let s=new t.CommandRegistry,o=e.contextMenuRenderer,l=new r.ContextMenu({commands:s,renderer:o});this.commands=s,this.contextMenu=l,this.shell=e.shell}get started(){return this._delegate.promise}hasPlugin(e){return e in this._pluginMap}listPlugins(){return Object.keys(this._pluginMap)}registerPlugin(e){if(e.id in this._pluginMap)throw new Error(`Plugin '${e.id}' is already registered.`);let t=n.createPluginData(e);n.ensureNoCycle(t,this._pluginMap,this._serviceMap),t.provides&&this._serviceMap.set(t.provides,t.id),this._pluginMap[t.id]=t}registerPlugins(e){for(let t of e)this.registerPlugin(t)}activatePlugin(e){let t=this._pluginMap[e];if(!t)return Promise.reject(new Error(`Plugin '${e}' is not registered.`));if(t.activated)return Promise.resolve(void 0);if(t.promise)return t.promise;let i=t.requires.map((e=>this.resolveRequiredService(e))),r=t.optional.map((e=>this.resolveOptionalService(e))),n=i.concat(r);return t.promise=Promise.all(n).then((e=>t.activate.apply(void 0,[this,...e]))).then((e=>{t.service=e,t.activated=!0,t.promise=null})).catch((e=>{throw t.promise=null,e})),t.promise}resolveRequiredService(e){let t=this._serviceMap.get(e);if(!t)return Promise.reject(new Error(`No provider for: ${e.name}.`));let i=this._pluginMap[t];return i.activated?Promise.resolve(i.service):this.activatePlugin(t).then((()=>i.service))}resolveOptionalService(e){let t=this._serviceMap.get(e);if(!t)return Promise.resolve(null);let i=this._pluginMap[t];return i.activated?Promise.resolve(i.service):this.activatePlugin(t).then((()=>i.service)).catch((e=>(console.error(e),null)))}start(e={}){if(this._started)return this._delegate.promise;this._started=!0;let t=e.hostID||"",i=n.collectStartupPlugins(this._pluginMap,e).map((e=>this.activatePlugin(e).catch((t=>{console.error(`Plugin '${e}' failed to activate.`),console.error(t)}))));return Promise.all(i).then((()=>{this.attachShell(t),this.addEventListeners(),this._delegate.resolve(void 0)})),this._delegate.promise}handleEvent(e){switch(e.type){case"resize":this.evtResize(e);break;case"keydown":this.evtKeydown(e);break;case"contextmenu":this.evtContextMenu(e)}}attachShell(e){r.Widget.attach(this.shell,e&&document.getElementById(e)||document.body)}addEventListeners(){document.addEventListener("contextmenu",this),document.addEventListener("keydown",this,!0),window.addEventListener("resize",this)}evtKeydown(e){this.commands.processKeydownEvent(e)}evtContextMenu(e){e.shiftKey||this.contextMenu.open(e)&&(e.preventDefault(),e.stopPropagation())}evtResize(e){this.shell.update()}},Object.defineProperty(e,"__esModule",{value:!0})})); | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("@lumino/commands"),require("@lumino/coreutils"),require("@lumino/widgets")):"function"==typeof define&&define.amd?define(["exports","@lumino/commands","@lumino/coreutils","@lumino/widgets"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).lumino_application={},t.lumino_commands,t.lumino_coreutils,t.lumino_widgets)}(this,(function(t,e,n,i){"use strict";function r(t,e,n,i){return new(n||(n=Promise))((function(r,o){function s(t){try{u(i.next(t))}catch(t){o(t)}}function l(t){try{u(i.throw(t))}catch(t){o(t)}}function u(t){var e;t.done?r(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(s,l)}u((i=i.apply(t,e||[])).next())}))}var o,s,l,u;!function(t){function e(t,e,n=0,i=-1){let r,o=t.length;if(0===o)return-1;n=n<0?Math.max(0,n+o):Math.min(n,o-1),r=(i=i<0?Math.max(0,i+o):Math.min(i,o-1))<n?i+1+(o-n):i-n+1;for(let i=0;i<r;++i){let r=(n+i)%o;if(t[r]===e)return r}return-1}function n(t,e,n=-1,i=0){let r,o=t.length;if(0===o)return-1;r=(n=n<0?Math.max(0,n+o):Math.min(n,o-1))<(i=i<0?Math.max(0,i+o):Math.min(i,o-1))?n+1+(o-i):n-i+1;for(let i=0;i<r;++i){let r=(n-i+o)%o;if(t[r]===e)return r}return-1}function i(t,e,n=0,i=-1){let r,o=t.length;if(0===o)return-1;n=n<0?Math.max(0,n+o):Math.min(n,o-1),r=(i=i<0?Math.max(0,i+o):Math.min(i,o-1))<n?i+1+(o-n):i-n+1;for(let i=0;i<r;++i){let r=(n+i)%o;if(e(t[r],r))return r}return-1}function r(t,e,n=-1,i=0){let r,o=t.length;if(0===o)return-1;r=(n=n<0?Math.max(0,n+o):Math.min(n,o-1))<(i=i<0?Math.max(0,i+o):Math.min(i,o-1))?n+1+(o-i):n-i+1;for(let i=0;i<r;++i){let r=(n-i+o)%o;if(e(t[r],r))return r}return-1}function o(t,e=0,n=-1){let i=t.length;if(!(i<=1))for(e=e<0?Math.max(0,e+i):Math.min(e,i-1),n=n<0?Math.max(0,n+i):Math.min(n,i-1);e<n;){let i=t[e],r=t[n];t[e++]=r,t[n--]=i}}function s(t,e){let n=t.length;if(e<0&&(e+=n),e<0||e>=n)return;let i=t[e];for(let i=e+1;i<n;++i)t[i-1]=t[i];return t.length=n-1,i}t.firstIndexOf=e,t.lastIndexOf=n,t.findFirstIndex=i,t.findLastIndex=r,t.findFirstValue=function(t,e,n=0,r=-1){let o=i(t,e,n,r);return-1!==o?t[o]:void 0},t.findLastValue=function(t,e,n=-1,i=0){let o=r(t,e,n,i);return-1!==o?t[o]:void 0},t.lowerBound=function(t,e,n,i=0,r=-1){let o=t.length;if(0===o)return 0;let s=i=i<0?Math.max(0,i+o):Math.min(i,o-1),l=(r=r<0?Math.max(0,r+o):Math.min(r,o-1))-i+1;for(;l>0;){let i=l>>1,r=s+i;n(t[r],e)<0?(s=r+1,l-=i+1):l=i}return s},t.upperBound=function(t,e,n,i=0,r=-1){let o=t.length;if(0===o)return 0;let s=i=i<0?Math.max(0,i+o):Math.min(i,o-1),l=(r=r<0?Math.max(0,r+o):Math.min(r,o-1))-i+1;for(;l>0;){let i=l>>1,r=s+i;n(t[r],e)>0?l=i:(s=r+1,l-=i+1)}return s},t.shallowEqual=function(t,e,n){if(t===e)return!0;if(t.length!==e.length)return!1;for(let i=0,r=t.length;i<r;++i)if(n?!n(t[i],e[i]):t[i]!==e[i])return!1;return!0},t.slice=function(t,e={}){let{start:n,stop:i,step:r}=e;if(void 0===r&&(r=1),0===r)throw new Error("Slice `step` cannot be zero.");let o,s=t.length;void 0===n?n=r<0?s-1:0:n<0?n=Math.max(n+s,r<0?-1:0):n>=s&&(n=r<0?s-1:s),void 0===i?i=r<0?-1:s:i<0?i=Math.max(i+s,r<0?-1:0):i>=s&&(i=r<0?s-1:s),o=r<0&&i>=n||r>0&&n>=i?0:r<0?Math.floor((i-n+1)/r+1):Math.floor((i-n-1)/r+1);let l=[];for(let e=0;e<o;++e)l[e]=t[n+e*r];return l},t.move=function(t,e,n){let i=t.length;if(i<=1)return;if((e=e<0?Math.max(0,e+i):Math.min(e,i-1))===(n=n<0?Math.max(0,n+i):Math.min(n,i-1)))return;let r=t[e],o=e<n?1:-1;for(let i=e;i!==n;i+=o)t[i]=t[i+o];t[n]=r},t.reverse=o,t.rotate=function(t,e,n=0,i=-1){let r=t.length;if(r<=1)return;if((n=n<0?Math.max(0,n+r):Math.min(n,r-1))>=(i=i<0?Math.max(0,i+r):Math.min(i,r-1)))return;let s=i-n+1;if(e>0?e%=s:e<0&&(e=(e%s+s)%s),0===e)return;let l=n+e;o(t,n,l-1),o(t,l,i),o(t,n,i)},t.fill=function(t,e,n=0,i=-1){let r,o=t.length;if(0!==o){n=n<0?Math.max(0,n+o):Math.min(n,o-1),r=(i=i<0?Math.max(0,i+o):Math.min(i,o-1))<n?i+1+(o-n):i-n+1;for(let i=0;i<r;++i)t[(n+i)%o]=e}},t.insert=function(t,e,n){let i=t.length;e=e<0?Math.max(0,e+i):Math.min(e,i);for(let n=i;n>e;--n)t[n]=t[n-1];t[e]=n},t.removeAt=s,t.removeFirstOf=function(t,n,i=0,r=-1){let o=e(t,n,i,r);return-1!==o&&s(t,o),o},t.removeLastOf=function(t,e,i=-1,r=0){let o=n(t,e,i,r);return-1!==o&&s(t,o),o},t.removeAllOf=function(t,e,n=0,i=-1){let r=t.length;if(0===r)return 0;n=n<0?Math.max(0,n+r):Math.min(n,r-1),i=i<0?Math.max(0,i+r):Math.min(i,r-1);let o=0;for(let s=0;s<r;++s)n<=i&&s>=n&&s<=i&&t[s]===e||i<n&&(s<=i||s>=n)&&t[s]===e?o++:o>0&&(t[s-o]=t[s]);return o>0&&(t.length=r-o),o},t.removeFirstWhere=function(t,e,n=0,r=-1){let o,l=i(t,e,n,r);return-1!==l&&(o=s(t,l)),{index:l,value:o}},t.removeLastWhere=function(t,e,n=-1,i=0){let o,l=r(t,e,n,i);return-1!==l&&(o=s(t,l)),{index:l,value:o}},t.removeAllWhere=function(t,e,n=0,i=-1){let r=t.length;if(0===r)return 0;n=n<0?Math.max(0,n+r):Math.min(n,r-1),i=i<0?Math.max(0,i+r):Math.min(i,r-1);let o=0;for(let s=0;s<r;++s)n<=i&&s>=n&&s<=i&&e(t[s],s)||i<n&&(s<=i||s>=n)&&e(t[s],s)?o++:o>0&&(t[s-o]=t[s]);return o>0&&(t.length=r-o),o}}(o||(o={})),function(t){t.rangeLength=function(t,e,n){return 0===n?1/0:t>e&&n>0||t<e&&n<0?0:Math.ceil((e-t)/n)}}(s||(s={})),function(t){function e(t,e,n=0){let i=new Array(e.length);for(let r=0,o=n,s=e.length;r<s;++r,++o){if(o=t.indexOf(e[r],o),-1===o)return null;i[r]=o}return i}t.findIndices=e,t.matchSumOfSquares=function(t,n,i=0){let r=e(t,n,i);if(!r)return null;let o=0;for(let t=0,e=r.length;t<e;++t){let e=r[t]-i;o+=e*e}return{score:o,indices:r}},t.matchSumOfDeltas=function(t,n,i=0){let r=e(t,n,i);if(!r)return null;let o=0,s=i-1;for(let t=0,e=r.length;t<e;++t){let e=r[t];o+=e-s-1,s=e}return{score:o,indices:r}},t.highlight=function(t,e,n){let i=[],r=0,o=0,s=e.length;for(;r<s;){let l=e[r],u=e[r];for(;++r<s&&e[r]===u+1;)u++;o<l&&i.push(t.slice(o,l)),l<u+1&&i.push(n(t.slice(l,u+1))),o=u+1}return o<t.length&&i.push(t.slice(o)),i},t.cmp=function(t,e){return t<e?-1:t>e?1:0}}(l||(l={}));!function(t){t.createPluginData=function(t){var e,n,i;return{id:t.id,service:null,promise:null,activated:!1,activate:t.activate,deactivate:null!==(e=t.deactivate)&&void 0!==e?e:null,provides:null!==(n=t.provides)&&void 0!==n?n:null,autoStart:null!==(i=t.autoStart)&&void 0!==i&&i,requires:t.requires?t.requires.slice():[],optional:t.optional?t.optional.slice():[]}},t.ensureNoCycle=function(t,e,n){const i=[...t.requires,...t.optional],r=i=>{if(i===t.provides)return!0;const s=n.get(i);if(!s)return!1;const l=e.get(s),u=[...l.requires,...l.optional];return 0!==u.length&&(o.push(s),!!u.some(r)||(o.pop(),!1))};if(!t.provides||0===i.length)return;const o=[t.id];if(i.some(r))throw new ReferenceError(`Cycle detected: ${o.join(" -> ")}.`)},t.findDependents=function(t,e,n){const i=new Array,r=t=>{const r=e.get(t),o=[...r.requires,...r.optional];i.push(...o.reduce(((e,i)=>{const r=n.get(i);return r&&e.push([t,r]),e}),[]))};for(const t of e.keys())r(t);const o=function(t){let e=[],n=new Set,i=new Map;for(const e of t)r(e);for(const[t]of i)o(t);return e;function r(t){let[e,n]=t,r=i.get(n);r?r.push(e):i.set(n,[e])}function o(t){if(n.has(t))return;n.add(t);let r=i.get(t);if(r)for(const t of r)o(t);e.push(t)}}(i),s=o.findIndex((e=>e===t));return-1===s?[t]:o.slice(0,s+1)},t.collectStartupPlugins=function(t,e){const n=new Map;for(const e in t)t.get(e).autoStart&&n.set(e,!0);if(e.startPlugins)for(const t of e.startPlugins)n.set(t,!0);if(e.ignorePlugins)for(const t of e.ignorePlugins)n.delete(t);return Array.from(n.keys())}}(u||(u={})),t.Application=class{constructor(t){this._delegate=new n.PromiseDelegate,this._plugins=new Map,this._services=new Map,this._started=!1,this.commands=new e.CommandRegistry,this.contextMenu=new i.ContextMenu({commands:this.commands,renderer:t.contextMenuRenderer}),this.shell=t.shell}get started(){return this._delegate.promise}hasPlugin(t){return this._plugins.has(t)}isPluginActivated(t){var e,n;return null!==(n=null===(e=this._plugins.get(t))||void 0===e?void 0:e.activated)&&void 0!==n&&n}listPlugins(){return Array.from(this._plugins.keys())}registerPlugin(t){if(this._plugins.has(t.id))throw new TypeError(`Plugin '${t.id}' is already registered.`);const e=u.createPluginData(t);u.ensureNoCycle(e,this._plugins,this._services),e.provides&&this._services.set(e.provides,e.id),this._plugins.set(e.id,e)}registerPlugins(t){for(const e of t)this.registerPlugin(e)}deregisterPlugin(t,e){const n=this._plugins.get(t);if(n){if(n.activated&&!e)throw new Error(`Plugin '${t}' is still active.`);this._plugins.delete(t)}}activatePlugin(t){return r(this,void 0,void 0,(function*(){const e=this._plugins.get(t);if(!e)throw new ReferenceError(`Plugin '${t}' is not registered.`);if(e.activated)return;if(e.promise)return e.promise;const n=e.requires.map((t=>this.resolveRequiredService(t))),i=e.optional.map((t=>this.resolveOptionalService(t)));return e.promise=Promise.all([...n,...i]).then((t=>e.activate.apply(void 0,[this,...t]))).then((t=>{e.service=t,e.activated=!0,e.promise=null})).catch((t=>{throw e.promise=null,t})),e.promise}))}deactivatePlugin(t){return r(this,void 0,void 0,(function*(){const e=this._plugins.get(t);if(!e)throw new ReferenceError(`Plugin '${t}' is not registered.`);if(!e.activated)return[];if(!e.deactivate)throw new TypeError(`Plugin '${t}'#deactivate() method missing`);const n=u.findDependents(t,this._plugins,this._services),i=n.map((t=>this._plugins.get(t)));for(const e of i)if(!e.deactivate)throw new TypeError(`Plugin ${e.id}#deactivate() method missing (depends on ${t})`);for(const t of i){const e=[...t.requires,...t.optional].map((t=>{const e=this._services.get(t);return e?this._plugins.get(e).service:null}));yield t.deactivate(this,...e),t.service=null,t.activated=!1}return n.pop(),n}))}resolveRequiredService(t){return r(this,void 0,void 0,(function*(){const e=this._services.get(t);if(!e)throw new TypeError(`No provider for: ${t.name}.`);const n=this._plugins.get(e);return n.activated||(yield this.activatePlugin(e)),n.service}))}resolveOptionalService(t){return r(this,void 0,void 0,(function*(){const e=this._services.get(t);if(!e)return null;const n=this._plugins.get(e);if(!n.activated)try{yield this.activatePlugin(e)}catch(t){return console.error(t),null}return n.service}))}start(t={}){if(this._started)return this._delegate.promise;this._started=!0;const e=t.hostID||"",n=u.collectStartupPlugins(this._plugins,t).map((t=>this.activatePlugin(t).catch((e=>{console.error(`Plugin '${t}' failed to activate.`),console.error(e)}))));return Promise.all(n).then((()=>{this.attachShell(e),this.addEventListeners(),this._delegate.resolve()})),this._delegate.promise}handleEvent(t){switch(t.type){case"resize":this.evtResize(t);break;case"keydown":this.evtKeydown(t);break;case"contextmenu":this.evtContextMenu(t)}}attachShell(t){i.Widget.attach(this.shell,t&&document.getElementById(t)||document.body)}addEventListeners(){document.addEventListener("contextmenu",this),document.addEventListener("keydown",this,!0),window.addEventListener("resize",this)}evtKeydown(t){this.commands.processKeydownEvent(t)}evtContextMenu(t){t.shiftKey||this.contextMenu.open(t)&&(t.preventDefault(),t.stopPropagation())}evtResize(t){this.shell.update()}},Object.defineProperty(t,"__esModule",{value:!0})})); | ||
//# sourceMappingURL=index.min.js.map |
{ | ||
"name": "@lumino/application", | ||
"version": "2.0.0-alpha.1", | ||
"version": "2.0.0-alpha.2", | ||
"description": "Lumino Pluggable Application", | ||
@@ -46,5 +46,5 @@ "homepage": "https://github.com/jupyterlab/lumino", | ||
"dependencies": { | ||
"@lumino/commands": "^2.0.0-alpha.1", | ||
"@lumino/coreutils": "^2.0.0-alpha.1", | ||
"@lumino/widgets": "^2.0.0-alpha.1" | ||
"@lumino/commands": "^2.0.0-alpha.2", | ||
"@lumino/coreutils": "^2.0.0-alpha.2", | ||
"@lumino/widgets": "^2.0.0-alpha.2" | ||
}, | ||
@@ -51,0 +51,0 @@ "devDependencies": { |
424
src/index.ts
@@ -10,2 +10,4 @@ // Copyright (c) Jupyter Development Team. | ||
|----------------------------------------------------------------------------*/ | ||
import { topologicSort } from '@lumino/algorithm'; | ||
import { CommandRegistry } from '@lumino/commands'; | ||
@@ -20,2 +22,6 @@ | ||
* | ||
* @typeparam T - The type for the application. | ||
* | ||
* @typeparam U - The service type, if the plugin `provides` one. | ||
* | ||
* #### Notes | ||
@@ -32,5 +38,5 @@ * Plugins are the foundation for building an extensible application. | ||
*/ | ||
export interface IPlugin<T, U> { | ||
export interface IPlugin<T extends Application, U> { | ||
/** | ||
* The human readable id of the plugin. | ||
* The human readable ID of the plugin. | ||
* | ||
@@ -85,3 +91,3 @@ * #### Notes | ||
*/ | ||
provides?: Token<U>; | ||
provides?: Token<U> | null; | ||
@@ -105,3 +111,12 @@ /** | ||
*/ | ||
activate: (app: T, ...args: any[]) => U | Promise<U>; | ||
activate: (app: T, ...args: Token<any>[]) => U | Promise<U>; | ||
/** | ||
* A function invoked to deactivate the plugin. | ||
* | ||
* @param app - The application which owns the plugin. | ||
* | ||
* @param args - The services specified by the `requires` property. | ||
*/ | ||
deactivate?: ((app: T, ...args: Token<any>[]) => void | Promise<void>) | null; | ||
} | ||
@@ -112,2 +127,4 @@ | ||
* | ||
* @typeparam T - The type of the application shell. | ||
* | ||
* #### Notes | ||
@@ -118,3 +135,3 @@ * The `Application` class is useful when creating large, complex | ||
*/ | ||
export class Application<T extends Widget> { | ||
export class Application<T extends Widget = Widget> { | ||
/** | ||
@@ -126,12 +143,8 @@ * Construct a new application. | ||
constructor(options: Application.IOptions<T>) { | ||
// Create the application command registry. | ||
let commands = new CommandRegistry(); | ||
// Create the application context menu. | ||
let renderer = options.contextMenuRenderer; | ||
let contextMenu = new ContextMenu({ commands, renderer }); | ||
// Initialize the application state. | ||
this.commands = commands; | ||
this.contextMenu = contextMenu; | ||
this.commands = new CommandRegistry(); | ||
this.contextMenu = new ContextMenu({ | ||
commands: this.commands, | ||
renderer: options.contextMenuRenderer | ||
}); | ||
this.shell = options.shell; | ||
@@ -174,3 +187,3 @@ } | ||
* | ||
* @param id - The id of the plugin of interest. | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
@@ -180,6 +193,17 @@ * @returns `true` if the plugin is registered, `false` otherwise. | ||
hasPlugin(id: string): boolean { | ||
return id in this._pluginMap; | ||
return this._plugins.has(id); | ||
} | ||
/** | ||
* Test whether a plugin is activated with the application. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns `true` if the plugin is activated, `false` otherwise. | ||
*/ | ||
isPluginActivated(id: string): boolean { | ||
return this._plugins.get(id)?.activated ?? false; | ||
} | ||
/** | ||
* List the IDs of the plugins registered with the application. | ||
@@ -190,3 +214,3 @@ * | ||
listPlugins(): string[] { | ||
return Object.keys(this._pluginMap); | ||
return Array.from(this._plugins.keys()); | ||
} | ||
@@ -200,3 +224,3 @@ | ||
* #### Notes | ||
* An error will be thrown if a plugin with the same id is already | ||
* An error will be thrown if a plugin with the same ID is already | ||
* registered, or if the plugin has a circular dependency. | ||
@@ -208,20 +232,20 @@ * | ||
registerPlugin(plugin: IPlugin<this, any>): void { | ||
// Throw an error if the plugin id is already registered. | ||
if (plugin.id in this._pluginMap) { | ||
throw new Error(`Plugin '${plugin.id}' is already registered.`); | ||
// Throw an error if the plugin ID is already registered. | ||
if (this._plugins.has(plugin.id)) { | ||
throw new TypeError(`Plugin '${plugin.id}' is already registered.`); | ||
} | ||
// Create the normalized plugin data. | ||
let data = Private.createPluginData(plugin); | ||
const data = Private.createPluginData(plugin); | ||
// Ensure the plugin does not cause a cyclic dependency. | ||
Private.ensureNoCycle(data, this._pluginMap, this._serviceMap); | ||
Private.ensureNoCycle(data, this._plugins, this._services); | ||
// Add the service token to the service map. | ||
if (data.provides) { | ||
this._serviceMap.set(data.provides, data.id); | ||
this._services.set(data.provides, data.id); | ||
} | ||
// Add the plugin to the plugin map. | ||
this._pluginMap[data.id] = data; | ||
this._plugins.set(data.id, data); | ||
} | ||
@@ -238,3 +262,3 @@ | ||
registerPlugins(plugins: IPlugin<this, any>[]): void { | ||
for (let plugin of plugins) { | ||
for (const plugin of plugins) { | ||
this.registerPlugin(plugin); | ||
@@ -245,47 +269,62 @@ } | ||
/** | ||
* Activate the plugin with the given id. | ||
* Deregister a plugin with the application. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @param force - Whether to deregister the plugin even if it is active. | ||
*/ | ||
deregisterPlugin(id: string, force?: boolean): void { | ||
const plugin = this._plugins.get(id); | ||
if (!plugin) { | ||
return; | ||
} | ||
if (plugin.activated && !force) { | ||
throw new Error(`Plugin '${id}' is still active.`); | ||
} | ||
this._plugins.delete(id); | ||
} | ||
/** | ||
* Activate the plugin with the given ID. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns A promise which resolves when the plugin is activated | ||
* or rejects with an error if it cannot be activated. | ||
*/ | ||
activatePlugin(id: string): Promise<void> { | ||
async activatePlugin(id: string): Promise<void> { | ||
// Reject the promise if the plugin is not registered. | ||
let data = this._pluginMap[id]; | ||
if (!data) { | ||
return Promise.reject(new Error(`Plugin '${id}' is not registered.`)); | ||
const plugin = this._plugins.get(id); | ||
if (!plugin) { | ||
throw new ReferenceError(`Plugin '${id}' is not registered.`); | ||
} | ||
// Resolve immediately if the plugin is already activated. | ||
if (data.activated) { | ||
return Promise.resolve(undefined); | ||
if (plugin.activated) { | ||
return; | ||
} | ||
// Return the pending resolver promise if it exists. | ||
if (data.promise) { | ||
return data.promise; | ||
if (plugin.promise) { | ||
return plugin.promise; | ||
} | ||
// Resolve the required services for the plugin. | ||
let required = data.requires.map(t => this.resolveRequiredService(t)); | ||
const required = plugin.requires.map(t => this.resolveRequiredService(t)); | ||
// Resolve the optional services for the plugin. | ||
let optional = data.optional.map(t => this.resolveOptionalService(t)); | ||
const optional = plugin.optional.map(t => this.resolveOptionalService(t)); | ||
// Create the array of promises to resolve. | ||
let promises = required.concat(optional); | ||
// Setup the resolver promise for the plugin. | ||
data.promise = Promise.all(promises) | ||
.then(services => { | ||
return data.activate.apply(undefined, [this, ...services]); | ||
}) | ||
plugin.promise = Promise.all([...required, ...optional]) | ||
.then(services => plugin!.activate.apply(undefined, [this, ...services])) | ||
.then(service => { | ||
data.service = service; | ||
data.activated = true; | ||
data.promise = null; | ||
plugin!.service = service; | ||
plugin!.activated = true; | ||
plugin!.promise = null; | ||
}) | ||
.catch(error => { | ||
data.promise = null; | ||
plugin!.promise = null; | ||
throw error; | ||
@@ -295,6 +334,62 @@ }); | ||
// Return the pending resolver promise. | ||
return data.promise; | ||
return plugin.promise; | ||
} | ||
/** | ||
* Deactivate the plugin and its downstream dependents if and only if the | ||
* plugin and its dependents all support `deactivate`. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns A list of IDs of downstream plugins deactivated with this one. | ||
*/ | ||
async deactivatePlugin(id: string): Promise<string[]> { | ||
// Reject the promise if the plugin is not registered. | ||
const plugin = this._plugins.get(id); | ||
if (!plugin) { | ||
throw new ReferenceError(`Plugin '${id}' is not registered.`); | ||
} | ||
// Bail early if the plugin is not activated. | ||
if (!plugin.activated) { | ||
return []; | ||
} | ||
// Check that this plugin can deactivate. | ||
if (!plugin.deactivate) { | ||
throw new TypeError(`Plugin '${id}'#deactivate() method missing`); | ||
} | ||
// Find the optimal deactivation order for plugins downstream of this one. | ||
const manifest = Private.findDependents(id, this._plugins, this._services); | ||
const downstream = manifest.map(id => this._plugins.get(id)!); | ||
// Check that all downstream plugins can deactivate. | ||
for (const plugin of downstream) { | ||
if (!plugin.deactivate) { | ||
throw new TypeError( | ||
`Plugin ${plugin.id}#deactivate() method missing (depends on ${id})` | ||
); | ||
} | ||
} | ||
// Deactivate all downstream plugins. | ||
for (const plugin of downstream) { | ||
const services = [...plugin.requires, ...plugin.optional].map(service => { | ||
const id = this._services.get(service); | ||
return id ? this._plugins.get(id)!.service : null; | ||
}); | ||
// Await deactivation so the next plugins only receive active services. | ||
await plugin.deactivate!(this, ...services); | ||
plugin.service = null; | ||
plugin.activated = false; | ||
} | ||
// Remove plugin ID and return manifest of deactivated plugins. | ||
manifest.pop(); | ||
return manifest; | ||
} | ||
/** | ||
* Resolve a required service of a given type. | ||
@@ -318,17 +413,16 @@ * | ||
*/ | ||
resolveRequiredService<U>(token: Token<U>): Promise<U> { | ||
async resolveRequiredService<U>(token: Token<U>): Promise<U> { | ||
// Reject the promise if there is no provider for the type. | ||
let id = this._serviceMap.get(token); | ||
const id = this._services.get(token); | ||
if (!id) { | ||
return Promise.reject(new Error(`No provider for: ${token.name}.`)); | ||
throw new TypeError(`No provider for: ${token.name}.`); | ||
} | ||
// Resolve immediately if the plugin is already activated. | ||
let data = this._pluginMap[id]; | ||
if (data.activated) { | ||
return Promise.resolve(data.service); | ||
// Activate the plugin if necessary. | ||
const plugin = this._plugins.get(id)!; | ||
if (!plugin.activated) { | ||
await this.activatePlugin(id); | ||
} | ||
// Otherwise, activate the plugin and wait on the results. | ||
return this.activatePlugin(id).then(() => data.service); | ||
return plugin.service; | ||
} | ||
@@ -355,24 +449,21 @@ | ||
*/ | ||
resolveOptionalService<U>(token: Token<U>): Promise<U | null> { | ||
async resolveOptionalService<U>(token: Token<U>): Promise<U | null> { | ||
// Resolve with `null` if there is no provider for the type. | ||
let id = this._serviceMap.get(token); | ||
const id = this._services.get(token); | ||
if (!id) { | ||
return Promise.resolve(null); | ||
return null; | ||
} | ||
// Resolve immediately if the plugin is already activated. | ||
let data = this._pluginMap[id]; | ||
if (data.activated) { | ||
return Promise.resolve(data.service); | ||
// Activate the plugin if necessary. | ||
const plugin = this._plugins.get(id)!; | ||
if (!plugin.activated) { | ||
try { | ||
await this.activatePlugin(id); | ||
} catch (reason) { | ||
console.error(reason); | ||
return null; | ||
} | ||
} | ||
// Otherwise, activate the plugin and wait on the results. | ||
return this.activatePlugin(id) | ||
.then(() => { | ||
return data.service; | ||
}) | ||
.catch(reason => { | ||
console.error(reason); | ||
return null; | ||
}); | ||
return plugin.service; | ||
} | ||
@@ -410,10 +501,10 @@ | ||
// Parse the host id for attaching the shell. | ||
let hostID = options.hostID || ''; | ||
// Parse the host ID for attaching the shell. | ||
const hostID = options.hostID || ''; | ||
// Collect the ids of the startup plugins. | ||
let startups = Private.collectStartupPlugins(this._pluginMap, options); | ||
const startups = Private.collectStartupPlugins(this._plugins, options); | ||
// Generate the activation promises. | ||
let promises = startups.map(id => { | ||
const promises = startups.map(id => { | ||
return this.activatePlugin(id).catch(error => { | ||
@@ -429,3 +520,3 @@ console.error(`Plugin '${id}' failed to activate.`); | ||
this.addEventListeners(); | ||
this._delegate.resolve(undefined); | ||
this._delegate.resolve(); | ||
}); | ||
@@ -456,3 +547,3 @@ | ||
case 'contextmenu': | ||
this.evtContextMenu(event as MouseEvent); | ||
this.evtContextMenu(event as PointerEvent); | ||
break; | ||
@@ -465,6 +556,6 @@ } | ||
* | ||
* @param id - The id of the host node for the shell, or `''`. | ||
* @param id - The ID of the host node for the shell, or `''`. | ||
* | ||
* #### Notes | ||
* If the id is not provided, the document body will be the host. | ||
* If the ID is not provided, the document body will be the host. | ||
* | ||
@@ -521,3 +612,3 @@ * A subclass may reimplement this method as needed. | ||
*/ | ||
protected evtContextMenu(event: MouseEvent): void { | ||
protected evtContextMenu(event: PointerEvent): void { | ||
if (event.shiftKey) { | ||
@@ -544,6 +635,6 @@ return; | ||
private _delegate = new PromiseDelegate<void>(); | ||
private _plugins = new Map<string, Private.IPluginData>(); | ||
private _services = new Map<Token<any>, string>(); | ||
private _started = false; | ||
private _pluginMap = Private.createPluginMap(); | ||
private _serviceMap = Private.createServiceMap(); | ||
private _delegate = new PromiseDelegate<void>(); | ||
} | ||
@@ -613,3 +704,3 @@ | ||
/** | ||
* The human readable id of the plugin. | ||
* The human readable ID of the plugin. | ||
*/ | ||
@@ -641,5 +732,12 @@ readonly id: string; | ||
*/ | ||
readonly activate: (app: any, ...args: any[]) => any; | ||
readonly activate: (app: Application, ...args: any[]) => any; | ||
/** | ||
* The optional function which deactivates the plugin. | ||
*/ | ||
readonly deactivate: | ||
| ((app: Application, ...args: any[]) => void | Promise<void>) | ||
| null; | ||
/** | ||
* Whether the plugin has been activated. | ||
@@ -661,26 +759,2 @@ */ | ||
/** | ||
* A type alias for a mapping of plugin id to plugin data. | ||
*/ | ||
export type PluginMap = { [id: string]: IPluginData }; | ||
/** | ||
* A type alias for a mapping of service token to plugin id. | ||
*/ | ||
export type ServiceMap = Map<Token<any>, string>; | ||
/** | ||
* Create a new plugin map. | ||
*/ | ||
export function createPluginMap(): PluginMap { | ||
return Object.create(null); | ||
} | ||
/** | ||
* Create a new service map. | ||
*/ | ||
export function createServiceMap(): ServiceMap { | ||
return new Map<Token<any>, string>(); | ||
} | ||
/** | ||
* Create a normalized plugin data object for the given plugin. | ||
@@ -695,4 +769,5 @@ */ | ||
activate: plugin.activate, | ||
provides: plugin.provides || null, | ||
autoStart: plugin.autoStart || false, | ||
deactivate: plugin.deactivate ?? null, | ||
provides: plugin.provides ?? null, | ||
autoStart: plugin.autoStart ?? false, | ||
requires: plugin.requires ? plugin.requires.slice() : [], | ||
@@ -709,35 +784,22 @@ optional: plugin.optional ? plugin.optional.slice() : [] | ||
export function ensureNoCycle( | ||
data: IPluginData, | ||
pluginMap: PluginMap, | ||
serviceMap: ServiceMap | ||
plugin: IPluginData, | ||
plugins: Map<string, IPluginData>, | ||
services: Map<Token<any>, string> | ||
): void { | ||
let dependencies = data.requires.concat(data.optional); | ||
// Bail early if there cannot be a cycle. | ||
if (!data.provides || dependencies.length === 0) { | ||
return; | ||
} | ||
// Setup a stack to trace service resolution. | ||
let trace = [data.id]; | ||
// Throw an exception if a cycle is present. | ||
if (dependencies.some(visit)) { | ||
throw new Error(`Cycle detected: ${trace.join(' -> ')}.`); | ||
} | ||
function visit(token: Token<any>): boolean { | ||
if (token === data.provides) { | ||
const dependencies = [...plugin.requires, ...plugin.optional]; | ||
const visit = (token: Token<any>): boolean => { | ||
if (token === plugin.provides) { | ||
return true; | ||
} | ||
let id = serviceMap.get(token); | ||
const id = services.get(token); | ||
if (!id) { | ||
return false; | ||
} | ||
let other = pluginMap[id]; | ||
let otherDependencies = other.requires.concat(other.optional); | ||
if (otherDependencies.length === 0) { | ||
const visited = plugins.get(id)!; | ||
const dependencies = [...visited.requires, ...visited.optional]; | ||
if (dependencies.length === 0) { | ||
return false; | ||
} | ||
trace.push(id); | ||
if (otherDependencies.some(visit)) { | ||
if (dependencies.some(visit)) { | ||
return true; | ||
@@ -747,19 +809,83 @@ } | ||
return false; | ||
}; | ||
// Bail early if there cannot be a cycle. | ||
if (!plugin.provides || dependencies.length === 0) { | ||
return; | ||
} | ||
// Setup a stack to trace service resolution. | ||
const trace = [plugin.id]; | ||
// Throw an exception if a cycle is present. | ||
if (dependencies.some(visit)) { | ||
throw new ReferenceError(`Cycle detected: ${trace.join(' -> ')}.`); | ||
} | ||
} | ||
/** | ||
* Find dependents in deactivation order. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @param plugins - The map containing all plugins. | ||
* | ||
* @param services - The map containing all services. | ||
* | ||
* @returns A list of dependent plugin IDs in order of deactivation | ||
* | ||
* #### Notes | ||
* The final item of the returned list is always the plugin of interest. | ||
*/ | ||
export function findDependents( | ||
id: string, | ||
plugins: Map<string, IPluginData>, | ||
services: Map<Token<any>, string> | ||
): string[] { | ||
const edges = new Array<[string, string]>(); | ||
const add = (id: string): void => { | ||
const plugin = plugins.get(id)!; | ||
// FIXME In the case of missing optional dependencies, we may consider | ||
// deactivating and reactivating the plugin without the missing service. | ||
const dependencies = [...plugin.requires, ...plugin.optional]; | ||
edges.push( | ||
...dependencies.reduce<[string, string][]>((acc, dep) => { | ||
const service = services.get(dep); | ||
if (service) { | ||
// An edge is oriented from dependent to provider. | ||
acc.push([id, service]); | ||
} | ||
return acc; | ||
}, []) | ||
); | ||
}; | ||
for (const id of plugins.keys()) { | ||
add(id); | ||
} | ||
const sorted = topologicSort(edges); | ||
const index = sorted.findIndex(candidate => candidate === id); | ||
if (index === -1) { | ||
return [id]; | ||
} | ||
return sorted.slice(0, index + 1); | ||
} | ||
/** | ||
* Collect the IDs of the plugins to activate on startup. | ||
*/ | ||
export function collectStartupPlugins( | ||
pluginMap: PluginMap, | ||
plugins: Map<string, IPluginData>, | ||
options: Application.IStartOptions | ||
): string[] { | ||
// Create a map to hold the plugin IDs. | ||
let resultMap: { [id: string]: boolean } = Object.create(null); | ||
const collection = new Map<string, boolean>(); | ||
// Collect the auto-start plugins. | ||
for (let id in pluginMap) { | ||
if (pluginMap[id].autoStart) { | ||
resultMap[id] = true; | ||
for (const id in plugins) { | ||
if (plugins.get(id)!.autoStart) { | ||
collection.set(id, true); | ||
} | ||
@@ -770,4 +896,4 @@ } | ||
if (options.startPlugins) { | ||
for (let id of options.startPlugins) { | ||
resultMap[id] = true; | ||
for (const id of options.startPlugins) { | ||
collection.set(id, true); | ||
} | ||
@@ -778,10 +904,10 @@ } | ||
if (options.ignorePlugins) { | ||
for (let id of options.ignorePlugins) { | ||
delete resultMap[id]; | ||
for (const id of options.ignorePlugins) { | ||
collection.delete(id); | ||
} | ||
} | ||
// Return the final startup plugins. | ||
return Object.keys(resultMap); | ||
// Return the collected startup plugins. | ||
return Array.from(collection.keys()); | ||
} | ||
} |
@@ -7,2 +7,6 @@ import { CommandRegistry } from '@lumino/commands'; | ||
* | ||
* @typeparam T - The type for the application. | ||
* | ||
* @typeparam U - The service type, if the plugin `provides` one. | ||
* | ||
* #### Notes | ||
@@ -19,5 +23,5 @@ * Plugins are the foundation for building an extensible application. | ||
*/ | ||
export interface IPlugin<T, U> { | ||
export interface IPlugin<T extends Application, U> { | ||
/** | ||
* The human readable id of the plugin. | ||
* The human readable ID of the plugin. | ||
* | ||
@@ -68,3 +72,3 @@ * #### Notes | ||
*/ | ||
provides?: Token<U>; | ||
provides?: Token<U> | null; | ||
/** | ||
@@ -87,3 +91,11 @@ * A function invoked to activate the plugin. | ||
*/ | ||
activate: (app: T, ...args: any[]) => U | Promise<U>; | ||
activate: (app: T, ...args: Token<any>[]) => U | Promise<U>; | ||
/** | ||
* A function invoked to deactivate the plugin. | ||
* | ||
* @param app - The application which owns the plugin. | ||
* | ||
* @param args - The services specified by the `requires` property. | ||
*/ | ||
deactivate?: ((app: T, ...args: Token<any>[]) => void | Promise<void>) | null; | ||
} | ||
@@ -93,2 +105,4 @@ /** | ||
* | ||
* @typeparam T - The type of the application shell. | ||
* | ||
* #### Notes | ||
@@ -99,3 +113,3 @@ * The `Application` class is useful when creating large, complex | ||
*/ | ||
export declare class Application<T extends Widget> { | ||
export declare class Application<T extends Widget = Widget> { | ||
/** | ||
@@ -135,3 +149,3 @@ * Construct a new application. | ||
* | ||
* @param id - The id of the plugin of interest. | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
@@ -142,2 +156,10 @@ * @returns `true` if the plugin is registered, `false` otherwise. | ||
/** | ||
* Test whether a plugin is activated with the application. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns `true` if the plugin is activated, `false` otherwise. | ||
*/ | ||
isPluginActivated(id: string): boolean; | ||
/** | ||
* List the IDs of the plugins registered with the application. | ||
@@ -154,3 +176,3 @@ * | ||
* #### Notes | ||
* An error will be thrown if a plugin with the same id is already | ||
* An error will be thrown if a plugin with the same ID is already | ||
* registered, or if the plugin has a circular dependency. | ||
@@ -172,6 +194,14 @@ * | ||
/** | ||
* Activate the plugin with the given id. | ||
* Deregister a plugin with the application. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @param force - Whether to deregister the plugin even if it is active. | ||
*/ | ||
deregisterPlugin(id: string, force?: boolean): void; | ||
/** | ||
* Activate the plugin with the given ID. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns A promise which resolves when the plugin is activated | ||
@@ -182,2 +212,11 @@ * or rejects with an error if it cannot be activated. | ||
/** | ||
* Deactivate the plugin and its downstream dependents if and only if the | ||
* plugin and its dependents all support `deactivate`. | ||
* | ||
* @param id - The ID of the plugin of interest. | ||
* | ||
* @returns A list of IDs of downstream plugins deactivated with this one. | ||
*/ | ||
deactivatePlugin(id: string): Promise<string[]>; | ||
/** | ||
* Resolve a required service of a given type. | ||
@@ -258,6 +297,6 @@ * | ||
* | ||
* @param id - The id of the host node for the shell, or `''`. | ||
* @param id - The ID of the host node for the shell, or `''`. | ||
* | ||
* #### Notes | ||
* If the id is not provided, the document body will be the host. | ||
* If the ID is not provided, the document body will be the host. | ||
* | ||
@@ -300,3 +339,3 @@ * A subclass may reimplement this method as needed. | ||
*/ | ||
protected evtContextMenu(event: MouseEvent): void; | ||
protected evtContextMenu(event: PointerEvent): void; | ||
/** | ||
@@ -311,6 +350,6 @@ * A method invoked on a window `'resize'` event. | ||
protected evtResize(event: Event): void; | ||
private _delegate; | ||
private _plugins; | ||
private _services; | ||
private _started; | ||
private _pluginMap; | ||
private _serviceMap; | ||
private _delegate; | ||
} | ||
@@ -317,0 +356,0 @@ /** |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
590367
5741
1