gristlabs_grist-core/app/server/lib/manifest.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

84 lines
2.7 KiB
TypeScript

import {BarePlugin} from 'app/plugin/PluginManifest';
import PluginManifestTI from 'app/plugin/PluginManifest-ti';
import * as fse from 'fs-extra';
import * as yaml from 'js-yaml';
import * as path from 'path';
import {createCheckers} from "ts-interface-checker";
const manifestChecker = createCheckers(PluginManifestTI).BarePlugin;
/**
* Validate the manifest and generate appropriate errors.
*/
// TODO: should validate that the resources referenced within the manifest are located within the
// plugin folder
// TODO: Need a comprehensive test that triggers every notices;
function isValidManifest(manifest: any, notices: string[]): boolean {
if (!manifest) {
notices.push("missing manifest");
return false;
}
try {
manifestChecker.check(manifest);
} catch (e) {
notices.push(`Invalid manifest: ${e.message}`);
return false;
}
try {
manifestChecker.strictCheck(manifest);
} catch (e) {
notices.push(`WARNING: ${e.message}` );
/* but don't fail */
}
if (Object.keys(manifest.contributions).length === 0) {
notices.push("WARNING: no valid contributions");
}
return true;
}
/**
* A ManifestError is an error caused by a wrongly formatted manifest or missing manifest. The
* `notices` property holds a user-friendly description of the error(s).
*/
export class ManifestError extends Error {
constructor(public notices: string[], message: string = "") {
super(message);
}
}
/**
* Parse the manifest. Look first for a Yaml manifest and then if missing for a Json manifest.
*/
export async function readManifest(pluginPath: string): Promise<BarePlugin> {
const notices: string[] = [];
const manifest: any = await _readManifest(pluginPath);
// We allow contributions and components to be omitted as shorthand
// for being the empty object.
if (!manifest.contributions) { manifest.contributions = {}; }
if (!manifest.components) { manifest.components = {}; }
if (isValidManifest(manifest, notices)) {
return manifest as BarePlugin;
}
throw new ManifestError(notices);
}
async function _readManifest(pluginPath: string): Promise<object> {
async function readManifestFile(fileExtension: string): Promise<string> {
return await fse.readFile(path.join(pluginPath, "manifest." + fileExtension), "utf8");
}
try {
return yaml.safeLoad(await readManifestFile("yml"));
} catch (e) {
if (e instanceof yaml.YAMLException) {
throw new Error('error parsing yaml manifest: ' + e.message);
}
}
try {
return JSON.parse(await readManifestFile("json"));
} catch (e) {
if (e instanceof SyntaxError) {
throw new Error('error parsing json manifest' + e.message);
}
throw new Error('cannot read manifest file: ' + e.message);
}
}