diff --git a/README.md b/README.md index 5118c49a..d2a38ad7 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ GRIST_SERVERS | the types of server to setup. Comma separated values which may c GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN GRIST_SESSION_SECRET | a key used to encode sessions +GRIST_SKIP_BUNDLED_WIDGETS | if set, Grist will ignore any bundled widgets included via NPM packages. GRIST_ANON_PLAYGROUND | When set to 'false' deny anonymous users access to the home page GRIST_FORCE_LOGIN | Much like GRIST_ANON_PLAYGROUND but don't support anonymous access at all (features like sharing docs publicly requires authentication) GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 0706bbd7..277d671e 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1998,8 +1998,13 @@ export class FlexServer implements GristServer { // Only used as {userRoot}/plugins as a place for plugins in addition to {appRoot}/plugins const userRoot = path.resolve(process.env.GRIST_USER_ROOT || getAppPathTo(this.appRoot, '.grist')); this.info.push(['userRoot', userRoot]); - - const pluginManager = new PluginManager(this.appRoot, userRoot); + // Some custom widgets may be included as an npm package called @gristlabs/grist-widget. + const bundledRoot = isAffirmative(process.env.GRIST_SKIP_BUNDLED_WIDGETS) ? undefined : path.join( + getAppPathTo(this.appRoot, 'node_modules'), + '@gristlabs', 'grist-widget', 'dist' + ); + this.info.push(['bundledRoot', bundledRoot]); + const pluginManager = new PluginManager(this.appRoot, userRoot, bundledRoot); // `initialize()` is asynchronous and reads plugins manifests; if PluginManager is used before it // finishes, it will act as if there are no plugins. // ^ I think this comment was here to justify calling initialize without waiting for diff --git a/app/server/lib/PluginEndpoint.ts b/app/server/lib/PluginEndpoint.ts index fa04db82..2d8bee14 100644 --- a/app/server/lib/PluginEndpoint.ts +++ b/app/server/lib/PluginEndpoint.ts @@ -46,7 +46,8 @@ function servePluginContent(req: express.Request, res: express.Response, req.get('X-From-Plugin-WebView') === "true" || mimeTypes.lookup(path.extname(pluginPath)) === "application/javascript") { const dirs = pluginManager.dirs(); - const contentRoot = pluginKind === "installed" ? dirs.installed : dirs.builtIn; + const contentRoot = pluginKind === "installed" ? dirs.installed : + (pluginKind === "builtIn" ? dirs.builtIn : dirs.bundled); // Note that pluginPath may not be safe, but `sendFile` with the "root" option restricts // relative paths to be within the root folder (see the 3rd party library unit-test: // https://github.com/pillarjs/send/blob/3daa901cf731b86187e4449fa2c52f971e0b3dbc/test/send.js#L1363) diff --git a/app/server/lib/PluginManager.ts b/app/server/lib/PluginManager.ts index 92861030..35b345d1 100644 --- a/app/server/lib/PluginManager.ts +++ b/app/server/lib/PluginManager.ts @@ -14,9 +14,14 @@ export interface PluginDirectories { */ readonly builtIn?: string; /** - * Directory where user installed plugins are localted. + * Directory where user installed plugins are located. */ readonly installed?: string; + /** + * Yet another option, for plugins that are included + * during a build but not part of the codebase itself. + */ + readonly bundled?: string; } /** @@ -44,10 +49,12 @@ export class PluginManager { * @param {string} userRoot: path to user's grist directory; `null` is allowed, to only uses built in plugins. * */ - public constructor(public appRoot?: string, userRoot?: string) { + public constructor(public appRoot?: string, userRoot?: string, + public bundledRoot?: string) { this._dirs = { installed: userRoot ? path.join(userRoot, 'plugins') : undefined, - builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined + builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined, + bundled: bundledRoot ? getAppPathTo(bundledRoot, 'plugins') : undefined, }; } @@ -91,6 +98,11 @@ export class PluginManager { this._entries.push(...await scanDirectory(this._dirs.builtIn, "builtIn")); } + // Load bundled plugins + if (this._dirs.bundled) { + this._entries.push(...await scanDirectory(this._dirs.bundled, "bundled")); + } + if (!process.env.GRIST_EXPERIMENTAL_PLUGINS || process.env.GRIST_EXPERIMENTAL_PLUGINS === '0') { // Remove experimental plugins @@ -130,7 +142,7 @@ export class PluginManager { } -async function scanDirectory(dir: string, kind: "installed"|"builtIn"): Promise { +async function scanDirectory(dir: string, kind: "installed"|"builtIn"|"bundled"): Promise { const plugins: DirectoryScanEntry[] = []; let listDir; diff --git a/package.json b/package.json index 0b6ba4e6..a68d2bc9 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@googleapis/oauth2": "0.2.0", "@gristlabs/connect-sqlite3": "0.9.11-grist.5", "@gristlabs/express-session": "1.17.0", + "@gristlabs/grist-widget": "^0.0.4", "@gristlabs/moment-guess": "1.2.4-grist.1", "@gristlabs/pidusage": "2.0.17", "@gristlabs/sqlite3": "5.1.4-grist.8", diff --git a/yarn.lock b/yarn.lock index a3e0202c..84797afd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -303,6 +303,11 @@ safe-buffer "5.2.0" uid-safe "~2.1.5" +"@gristlabs/grist-widget@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@gristlabs/grist-widget/-/grist-widget-0.0.4.tgz#df50d988bcdf8fc26a876cf23b82e258bbdb0ccc" + integrity sha512-Q0k+GuudU2+0JkuvVkB9UZzqeUKJH8PsaO9ZfxKuqL9/ssIXUd080msB+PJLXB0TU9BkpzPSl7+kLqXTBSnA5g== + "@gristlabs/moment-guess@1.2.4-grist.1": version "1.2.4-grist.1" resolved "https://registry.npmjs.org/@gristlabs/moment-guess/-/moment-guess-1.2.4-grist.1.tgz"