gristlabs_grist-core/app/plugin/PluginManifest.ts
Paul Fitzpatrick cc9a9ae8c5 (core) support for bundling custom widgets with the Grist app
Summary:
This adds support for bundling custom widgets with the Grist app, as follows:

 * Adds a new `widgets` component to plugins mechanism.
 * When a set of widgets is provided in a plugin, the html/js/css assets for those widgets are served on the existing untrusted user content port.
 * Any bundled `grist-plugin-api.js` will be served with the Grist app's own version of that file. It is important that bundled widgets not refer to https://docs.getgrist.com for the plugin js, since they must be capable of working offline.
 * The logic for configuring that port is updated a bit.
 * I removed the CustomAttachedView class in favor of applying settings of bundled custom widgets more directly, without modification on view.

Any Grist installation via docker will need an extra step now, since there is an extra port that needs exposing for full functionality. I did add a `GRIST_TRUST_PLUGINS` option for anyone who really doesn't want to do this, and would prefer to trust the plugins and have them served on the same port.

Actually making use of bundling will be another step. It'll be important to mesh it with our SaaS's use of APP_STATIC_URL for serving most static assets.

Design sketch: https://grist.quip.com/bJlWACWzr2R9/Bundled-custom-widgets

Test Plan: added a test

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4069
2023-10-27 17:00:10 -04:00

239 lines
9.2 KiB
TypeScript

/**
* This file defines the interface for a plugin manifest.
*
* Note that it is possible to validate a manifest against a TypeScript interface as follows:
* (1) Convert the interface to a JSON schema at build time using
* https://www.npmjs.com/package/typescript-json-schema:
* bin/typescript-json-schema --required --noExtraProps PluginManifest.ts PublishedPlugin
* (2) Use a JSON schema validator like https://www.npmjs.com/package/ajv to validate manifests
* read at run-time and produce informative errors automatically.
*
* TODO [Proposal]: To save an ImportSource for reuse, we would save:
* {
* pluginId: string;
* importSource: ImportSource;
* importProcessor?: Implementation;
* parseOptions?: ParseOptions; // If importProcessor is omitted and fileParser is used.
* }
* This should suffice for database re-imports, as well as for re-imports from a URL, or from a
* saved path in the filesystem (which can be a builtIn plugin available for Electron version).
*/
/**
* PublishedPlugin is a BarePlugin with additional attributes to identify and describe a plugin
* for publishing.
*/
export interface PublishedPlugin extends BarePlugin {
name: string;
version: string;
}
/**
* BarePlugin defines the functionality of a plugin. It is the only part required for a plugin to
* function, and is implemented by built-in plugins, published plugins, and private plugins (such
* as those being developed).
*/
export interface BarePlugin {
/**
* An optional human-readable name.
*/
name?: string;
/**
* Components describe how the plugin runs. A plugin may provide UI and behavior that runs in
* the browser, Python code that runs in a secure sandbox, and arbitrary code that runs in Node.
*/
components: {
/**
* Relative path to the directory whose content will be served to the browser. Required for
* those plugins that need to render their own HTML or run in-browser Javascript. This
* directory should contain all html files referenced in the manifest.
*
* It is "safe" in that Grist offers protections that allow such plugins to be marked "safe".
*/
safeBrowser?: string;
/**
* Relative path to a file with Python code that will be run in a python sandbox. This
* file is started on plugin activation, and should register any implemented APIs.
* Required for plugins that do Python processing.
*
* It is "safe" in that Grist offers protections that allow such plugins to be marked "safe".
*/
safePython?: string;
/**
* Relative path to a file containing Javascript code that will be executed with Node.js.
* The code is called on plugin activation, and should register any implemented APIs
* once we've figured out how that should happen (TODO). Required for plugins that need
* to do any "unsafe" work, such as accessing the local filesystem or starting helper
* programs.
*
* It is "unsafe" in that it can do too much, and Grist marks such plugins as "unsafe".
*
* An unsafeNode component opens as separate process to run plugin node code, with the
* NODE_PATH set to the plugin directory. The node code can execute arbitrary actions -
* there is no sandboxing.
*
* The node child may communicate with the server via standard ChildProcess ipc
* (`process.send`, `process.on('message', ...)`). The child is expected to
* `process.send` a message to the server once it is listening to the `message`
* event. That message is expected to contain a `ready` field set to `true`. All
* other communication should follow the protocol implemented by the Rpc module.
* TODO: provide plugin authors with documentation + library to use that implements
* these requirements.
*
*/
unsafeNode?: string;
/**
* Relative path to a specialized manifest of custom widgets.
* I'm unsure how this fits into components and contributions,
* this seemed the least-worst spot for it.
*/
widgets?: string;
/**
* Options for when to deactivate the plugin, i.e. when to stop any plugin processes. (Note
* that we may in the future also add options for when to activate the plugin, which is for
* now automatic and not configurable.)
*/
deactivate?: {
// Deactivate after this many seconds of inactivity. Defaults to 300 (5 minutes) if omitted.
inactivitySec?: number;
}
};
/**
* Contributions describe what new functionality the plugin contributes to the Grist
* application. See documentation for individual contribution types for details. Any plugin may
* provide multiple contributions. It is common to provide just one, in which case include a
* single property with a single-element array.
*/
contributions: {
importSources?: ImportSource[];
fileParsers?: FileParser[];
customSections?: CustomSection[];
};
/**
* Experimental plugins run only if the environment variable GRIST_EXPERIMENTAL_PLUGINS is
* set. Otherwise they are ignored. This is useful for plugins that needs a bit of experimentation
* before being pushed to production (ie: production does not have GRIST_EXPERIMENTAL_PLUGINS set
* but staging does). Keep in mind that developers need to set this environment if they want to
* run them locally.
*/
experimental?: boolean;
}
/**
* An ImportSource plugin creates a new source of imports, such as an external API, a file-sharing
* service, or a new type of database. It adds a new item for the user to select when importing.
*/
export interface ImportSource {
/**
* Label shows up as a new item for the user to select when starting an import.
*/
label: string;
/**
* Whether this import source can be exposed on a home screen for all users. Home imports
* support only a safeBrowser component and have no access to current document. Primarily used as
* an external/cloud storage providers.
*/
safeHome?: boolean;
/**
* Implementation of ImportSourceAPI. Supports safeBrowser component, which allows you to create
* custom UI to show to the user. Or describe UI using a .json or .yml config file and use
* {component: "builtIn", name: "importSourceConfig", path: "your-config"}.
*/
importSource: Implementation;
/**
* Implementation of ImportProcessorAPI. It receives the output of importSource, and produces
* Grist data. If omitted, uses the default ImportProcessor, which is equivalent to
* {component: "builtIn", name: "fileParser"}.
*
* The default ImportProcessor handles received ImportSourceItems as follows:
* (1) items of type "file" are saved to temp files.
* (2) items of type "url" are downloaded to temp files.
* (3) calls ParseFileAPI.parseFile() with all temp files, to produce Grist tables
* (4) returns those Grist tables along with all items of type "table".
* Note that the default ImportParser ignores ImportSource items of type "custom".
*/
importProcessor?: Implementation;
}
/**
* A FileParser plugin adds support to parse a new type of file data, such as "csv", "yml", or
* "ods". It then enables importing the new type of file via upload or from any other ImportSource
* that produces Files or URLs.
*/
export interface FileParser {
/**
* File extensions for which this FileParser should be considered, e.g. "csv", "yml". You may
* use "" for files with no extensions, and "*" to match any extension.
*/
fileExtensions: string[];
/**
* Implementation of EditOptionsAPI. Supports safeBrowser component, which allows you to create
* custom UI to show to the user. Or describe UI using a .json or .yml config file and use
* {component: "builtIn", name: "parseOptionsConfig", path: "your-config"}.
*
* If omitted, the user will be shown no parse options.
*/
editOptions?: Implementation;
/**
* Implementation of ParseFileAPI, which converts Files to Grist data using parse options.
*/
parseFile: Implementation;
}
/**
* A CustomSection plugin adds support to add new types of section to Grist, such as a calendar,
* maps, data visualizations.
*/
export interface CustomSection {
/**
* Path to an html file.
*/
path: string;
/**
* The name should uniquely identify the section in the plugin.
*/
name: string;
}
/**
* A Plugin supplies one or more Implementation of some APIs. Components register implementation
* using a call such as:
* grist.register(SomeAPI, 'myName', impl).
* The manifest documentation describes which API must be implemented at any particular point, and
* it is the plugin's responsibility to register an implementation of the correct API and refer to
* it by Implementation.name.
*/
export interface Implementation {
/**
* Which component of the plugin provides this implementation.
*/
component: "safeBrowser" | "safePython" | "unsafeNode";
/**
* The name of the implementation registered by the chosen component. The same component can
* register any number of APIs at any names.
*/
name: string;
/**
* Path is used by safeBrowser component for which page to load. Defaults to 'index.html'.
* It is also used by certain builtIn implementation, e.g. if name is 'parse-options-config',
* path is the path to JSON or YAML file containing the configuration.
*/
path?: string;
}