jupyterlab / lumino

Lumino is a library for building interactive web applications
https://lumino.readthedocs.io/
Other
598 stars 127 forks source link

Conditional Lumino plugin load #601

Open fcollonval opened 1 year ago

fcollonval commented 1 year ago

This is a draft that requires refinement.

To reduce the start-up time of JupyterLab, we could be smarter about plugin loading logic. For now, a Lumino plugin can be started automatically or not using the autoStart flag. But there is no way to specify conditions for which a plugin should start.

Before laying down a plan for Lumino plugin, here is an analysis of VS Code extension API that allows to conditionally load plugins. There are two key points:

In VS Code, those elements are defined in the extension package.json. The entry points are for example (see contributions documentation):

The trigger events are for example (see activation events documentation):

Back to JupyterLab and Lumino, the equivalent of onStartupFinished could be implemented quickly. But it may result in unwanted side effects; e.g. if the plugin is providing a widget and that widget is used in a notebook output that is loaded at startup, the widget may or may not be rendered. To ensure robustness, other events should be available; for example the widget plugin should load before opening a notebook.

Plugins adding new panels in the main area or in the side panel, like jupyterlab-git, are probably the best candidates for optimization. This requires defining side panels and main area widgets in a no-code way. In JupyterLab, we introduced the definition of keyboard shortcuts and menus in setting schema files. So it seems a more natural place than the extension package.json.

To be able to add a panel, widget titles need to be defined in a no-code way. This requires in particular the definition of icons in a no-code way.

Some first actions could be:

welcome[bot] commented 1 year ago

Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
If you haven't done so already, check out Jupyter's Code of Conduct. Also, please try to follow the issue template as it helps other other community members to contribute more effectively. welcome You can meet the other Jovyans by joining our Discourse forum. There is also an intro thread there where you can stop by and say Hi! :wave:
Welcome to the Jupyter community! :tada:

fcollonval commented 11 months ago

Discussion notes: https://hackmd.io/DBlDBDszSVSjYK9ieg1mZA

fcollonval commented 1 month ago

More thoughts about this for the GSoC project that will be implemented in JupyterLab repo directly - on a specific branch.

[!NOTE] This is a proposal to help kicking off an implementation

Vocabulary

What is a plugin entrypoint? An plugin entrypoint is no-code model defined as JSON that describes enough about a type of application feature to create a placeholder in the user interface. Then if the user interact with the placeholder, the JavaScript of the plugin will be loaded and its _activate_ function will be called to replace all placeholders (for that plugin) with the real implementation.
What is a plugin? It is object providing features in the application.
What is an extension? It is a NPM package providing one or more plugins.

Generic logic

A type of application feature is defined by an object provided by a plugin through a Token or an attribute of the Application. Other plugins may request that token to add a variant for that feature. For example, a application feature is the command that can be added through the CommandRegistry. Another example is a document viewer that can be added through the DocRegistry in JupyterLab. In the following I will name registry an object allowing to add a type of application feature.

Therefore the generic logic to use entrypoints is the following:

  1. List entrypoints per plugins from all extension package.json
  2. Store those entrypoints in the Application
  3. Registries supporting entrypoints will look at the entrypoints defined in the Application
  4. For each supported entrypoint, they will add a placeholder
  5. Each placeholder should be stored in the Application. So that the application can disposed them before activating the associated plugin.
  6. When an user triggered a placeholder:
    1. will load the plugin assets
    2. activate the plugin

A specific case

Let take the example of a new document viewer for CSV (interface IWidgetFactory) that is added to the DocRegistry using addWidgetFactory.

If the JSON schema of IWidgetFactory looks like:

{
    defaultFor?: readonly string[];
    defaultRendered?: readonly string[];
    fileTypes: readonly string[];
    label?: string;
    modelName?: string;
    name: string;
}

The placeholder interface will be something like:

interface IPlaceholder extends IObservableDisposable {}

The idea of using IObservableDisposable is to automatically trigger loading and activating the plugin by the Application when it is notified that a placeholder is disposed. But a dedicated method in IPlaceholder that returns a Promise resolving when the plugin is activated may be more appropriate to know when the actual call to the feature can be made.

The example could be

{
    "name": "CSVTable",
    "label": "CSV Viewer",
    "fileTypes": ["csv"],
    "defaultFor": ["csv"]
}
class WidgetFactoryPlaceholder implements IWidgetFactory, IPlaceholder {
  constructor(entrypoint: IWidgetFactoryPlaceholder) {
    // ...
  }

  createNew() {
    // This is actually this method that when triggered should load the plugin
  }
}

The DocRegistry would add the placeholder:

app.docRegistry.addWidgetFactory(new WidgetFactoryPlaceholder(entrypoint));

[!NOTE] One of the technical difficulty is to be able to redo the action triggered on the placeholder with the real code once the plugin is loaded. For the aforementioned example, this materializes in how to trigger the widget factory createNew method from the real code.