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

142 lines
4.4 KiB
TypeScript

/**
* Main entrypoint for grist-core server.
*
* By default, starts up on port 8484.
*/
import {commonUrls} from 'app/common/gristUrls';
import {isAffirmative} from 'app/common/gutil';
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {TEAM_FREE_PLAN} from 'app/common/Features';
const debugging = isAffirmative(process.env.DEBUG) || isAffirmative(process.env.VERBOSE);
// Set log levels before importing anything.
if (!debugging) {
// Be a lot less noisy by default.
setDefaultEnv('GRIST_LOG_LEVEL', 'error');
setDefaultEnv('GRIST_LOG_SKIP_HTTP', 'true');
}
// Use a distinct cookie. Bump version to 2.
setDefaultEnv('GRIST_SESSION_COOKIE', 'grist_core2');
setDefaultEnv('GRIST_SERVE_SAME_ORIGIN', 'true');
setDefaultEnv('GRIST_SINGLE_PORT', 'true');
setDefaultEnv('GRIST_DEFAULT_PRODUCT', 'Free');
if (!process.env.GRIST_SINGLE_ORG) {
// org identifiers in domains are fiddly to configure right, so by
// default don't do that.
setDefaultEnv('GRIST_ORG_IN_PATH', 'true');
}
setDefaultEnv('GRIST_UI_FEATURES', 'helpCenter,billing,templates,multiSite,multiAccounts,sendToDrive');
setDefaultEnv('GRIST_WIDGET_LIST_URL', commonUrls.gristLabsWidgetRepository);
import {updateDb} from 'app/server/lib/dbUtils';
import {main as mergedServerMain, parseServerTypes} from 'app/server/mergedServerMain';
import * as fse from 'fs-extra';
const G = {
port: parseInt(process.env.PORT!, 10) || 8484,
};
// Set a default for an environment variable.
function setDefaultEnv(name: string, value: string) {
if (process.env[name] === undefined) {
process.env[name] = value;
}
}
async function setupDb() {
// Make a blank db if needed.
if (process.env.TEST_CLEAN_DATABASE) {
const {createInitialDb} = require('test/gen-server/seed');
await createInitialDb();
} else {
await updateDb();
}
const db = new HomeDBManager();
await db.connect();
await db.initializeSpecialIds({skipWorkspaces: true});
// If a team/organization is specified, make sure it exists.
const org = process.env.GRIST_SINGLE_ORG;
if (org && org !== 'docs') {
try {
db.unwrapQueryResult(await db.getOrg({
userId: db.getPreviewerUserId(),
includeSupport: false,
}, org));
} catch(e) {
if (!String(e).match(/organization not found/)) {
throw e;
}
const email = process.env.GRIST_DEFAULT_EMAIL;
if (!email) {
throw new Error('need GRIST_DEFAULT_EMAIL to create site');
}
const profile = {email, name: email};
const user = await db.getUserByLogin(email, {profile});
if (!user) {
// This should not happen.
throw new Error('failed to create GRIST_DEFAULT_EMAIL user');
}
db.unwrapQueryResult(await db.addOrg(user, {
name: org,
domain: org,
}, {
setUserAsOwner: false,
useNewPlan: true,
planType: TEAM_FREE_PLAN
}));
}
}
}
// tslint:disable:no-console
export async function main() {
console.log('Welcome to Grist.');
if (!debugging) {
console.log(`In quiet mode, see http://localhost:${G.port} to use.`);
console.log('For full logs, re-run with DEBUG=1');
}
// If SAML is not configured, there's no login system, so provide a default email address.
setDefaultEnv('GRIST_DEFAULT_EMAIL', 'you@example.com');
// Set directory for uploaded documents.
setDefaultEnv('GRIST_DATA_DIR', 'docs');
setDefaultEnv('GRIST_SERVERS', 'home,docs,static');
if (process.env.GRIST_SERVERS?.includes('home')) {
// By default, we will now start an untrusted port alongside a
// home server, for bundled custom widgets.
// Suppress with GRIST_UNTRUSTED_PORT=''
setDefaultEnv('GRIST_UNTRUSTED_PORT', '0');
}
const serverTypes = parseServerTypes(process.env.GRIST_SERVERS);
await fse.mkdirp(process.env.GRIST_DATA_DIR!);
if (serverTypes.includes("home")) {
console.log('Setting up database...');
await setupDb();
console.log('Database setup complete.');
}
// Launch single-port, self-contained version of Grist.
const server = await mergedServerMain(G.port, serverTypes);
if (process.env.GRIST_TESTING_SOCKET) {
await server.addTestingHooks();
}
if (process.env.GRIST_SERVE_PLUGINS_PORT) {
await server.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10));
}
return server;
}
if (require.main === module) {
main().catch((err) => {
console.error(err);
});
}