(core) move home server into core

Summary: This moves enough server material into core to run a home server.  The data engine is not yet incorporated (though in manual testing it works when ported).

Test Plan: existing tests pass

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2552
This commit is contained in:
Paul Fitzpatrick
2020-07-21 09:20:51 -04:00
parent c756f663ee
commit 5ef889addd
218 changed files with 33640 additions and 38 deletions

View File

@@ -0,0 +1,11 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const CustomSectionAPI = t.iface([], {
"createSection": t.func("void", t.param("inlineTarget", "RenderTarget")),
});
const exportedTypeSuite: t.ITypeSuite = {
CustomSectionAPI,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,10 @@
/**
* API definitions for CustomSection plugins.
*/
import {RenderTarget} from './RenderOptions';
export interface CustomSectionAPI {
createSection(inlineTarget: RenderTarget): Promise<void>;
}

View File

@@ -0,0 +1,41 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const EditOptionsAPI = t.iface([], {
"getParseOptions": t.func("ParseOptions", t.param("parseOptions", "ParseOptions", true)),
});
export const ParseFileAPI = t.iface([], {
"parseFile": t.func("ParseFileResult", t.param("file", "FileSource"), t.param("parseOptions", "ParseOptions", true)),
});
export const ParseOptions = t.iface([], {
"NUM_ROWS": t.opt("number"),
"SCHEMA": t.opt(t.array("ParseOptionSchema")),
});
export const ParseOptionSchema = t.iface([], {
"name": "string",
"label": "string",
"type": "string",
"visible": "boolean",
});
export const FileSource = t.iface([], {
"path": "string",
"origName": "string",
});
export const ParseFileResult = t.iface(["GristTables"], {
"parseOptions": "ParseOptions",
});
const exportedTypeSuite: t.ITypeSuite = {
EditOptionsAPI,
ParseFileAPI,
ParseOptions,
ParseOptionSchema,
FileSource,
ParseFileResult,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,52 @@
/**
* API definitions for FileParser plugins.
*/
import {GristTables} from './GristTable';
export interface EditOptionsAPI {
getParseOptions(parseOptions?: ParseOptions): Promise<ParseOptions>;
}
export interface ParseFileAPI {
parseFile(file: FileSource, parseOptions?: ParseOptions): Promise<ParseFileResult>;
}
/**
* ParseOptions contains parse options depending on plugin,
* number of rows, which is special option that can be used for any plugin
* and schema for generating parse options UI
*/
export interface ParseOptions {
NUM_ROWS?: number;
SCHEMA?: ParseOptionSchema[];
}
/**
* ParseOptionSchema contains information for generaing parse options UI
*/
export interface ParseOptionSchema {
name: string;
label: string;
type: string;
visible: boolean;
}
export interface FileSource {
/**
* The path is often a temporary file, so its name is meaningless. Access to the file depends on
* the type of plugin. For instance, for `safePython` plugins file is directly available at
* `/importDir/path`.
*/
path: string;
/**
* Plugins that want to know the original filename should use origName. Depending on the source
* of the data, it may or may not be meaningful.
*/
origName: string;
}
export interface ParseFileResult extends GristTables {
parseOptions: ParseOptions;
}

34
app/plugin/GristAPI-ti.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* This module was automatically generated by `ts-interface-builder`
*/
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const ComponentKind = t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode"));
export const GristAPI = t.iface([], {
"render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"),
t.param("options", "RenderOptions", true)),
"dispose": t.func("void", t.param("procId", "number")),
"subscribe": t.func("void", t.param("tableId", "string")),
"unsubscribe": t.func("void", t.param("tableId", "string")),
});
export const GristDocAPI = t.iface([], {
"getDocName": t.func("string"),
"listTables": t.func(t.array("string")),
"fetchTable": t.func("any", t.param("tableId", "string")),
"applyUserActions": t.func("any", t.param("actions", t.array(t.array("any")))),
});
export const GristView = t.iface([], {
"fetchSelectedTable": t.func("any"),
});
const exportedTypeSuite: t.ITypeSuite = {
ComponentKind,
GristAPI,
GristDocAPI,
GristView,
};
export default exportedTypeSuite;

99
app/plugin/GristAPI.ts Normal file
View File

@@ -0,0 +1,99 @@
/**
* This file defines the interface for the grist api exposed to SafeBrowser plugins. Grist supports
* various ways to require it to cover various scenarios. If writing the main safeBrowser module
* (the one referenced by the components.safeBrowser key of the manifest) use
* `self.importScript('grist');`, if writing a view include the script in the html `<script src="grist"></script>`
*
*
* Example usage (let's assume that Grist let's plugin contributes to a Foo API defined as follow ):
*
* interface Foo {
* foo(name: string): Promise<string>;
* }
*
* > main.ts:
* class MyFoo {
* public foo(name: string): Promise<string> {
* return new Promise<string>( async resolve => {
* grist.rpc.onMessage( e => {
* resolve(e.data + name);
* });
* grist.ready();
* await grist.api.render('view1.html', 'fullscreen');
* });
* }
* }
* grist.rpc.registerImpl<Foo>('grist', new MyFoo()); // can add 3rd arg with type information
*
* > view1.html includes:
* grist.api.render('static/view2.html', 'fullscreen').then( view => {
* grist.rpc.onMessage(e => grist.rpc.postMessageForward("main.ts", e.data));
* });
*
* > view2.html includes:
* grist.rpc.postMessage('view1.html', 'foo ');
*
*/
import {RenderOptions, RenderTarget} from './RenderOptions';
export type ComponentKind = "safeBrowser" | "safePython" | "unsafeNode";
export const RPC_GRISTAPI_INTERFACE = '_grist_api';
export interface GristAPI {
/**
* Render the file at `path` into the `target` location in Grist. `path` must be relative to the
* root of the plugin's directory and point to an html that is contained within the plugin's
* directory. `target` is a predifined location of the Grist UI, it could be `fullscreen` or
* identifier for an inline target. Grist provides inline target identifiers in certain call
* plugins. E.g. ImportSourceAPI.getImportSource is given a target identifier to allow rende UI
* inline in the import dialog. Returns the procId which can be used to dispose the view.
*/
render(path: string, target: RenderTarget, options?: RenderOptions): Promise<number>;
/**
* Dispose the process with id procId. If the process was embedded into the UI, removes the
* corresponding element from the view.
*/
dispose(procId: number): Promise<void>;
// Subscribes to actions for `tableId`. Actions of all subscribed tables are send as rpc's
// message.
// TODO: document format of messages that can be listened on `rpc.onMessage(...);`
subscribe(tableId: string): Promise<void>;
// Unsubscribe from actions for `tableId`.
unsubscribe(tableId: string): Promise<void>;
}
/**
* GristDocAPI interface is implemented by Grist, and allows getting information from and
* interacting with the Grist document to which a plugin is attached.
*/
export interface GristDocAPI {
// Returns the docName that identifies the document.
getDocName(): Promise<string>;
// Returns a sorted list of table IDs.
listTables(): Promise<string[]>;
// Returns a complete table of data in the format {colId: [values]}, including the 'id' column.
// Do not modify the returned arrays in-place, especially if used directly (not over RPC).
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
// ts-interface-builder does not properly support index-signature.
fetchTable(tableId: string): Promise<any>;
// Applies an array of user actions.
// todo: return type should be Promise<ApplyUAResult>, but this requires importing modules from
// `app/common` which is not currently supported by the build.
applyUserActions(actions: any[][]): Promise<any>;
}
export interface GristView {
// Like fetchTable, but gets data for the custom section specifically, if there is any.
// TODO: return type is Promise{[colId: string]: CellValue[]}> but cannot be specified because
// ts-interface-builder does not properly support index-signature.
fetchSelectedTable(): Promise<any>;
}

View File

@@ -0,0 +1,24 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const GristTable = t.iface([], {
"table_name": t.union("string", "null"),
"column_metadata": t.array("GristColumn"),
"table_data": t.array(t.array("any")),
});
export const GristTables = t.iface([], {
"tables": t.array("GristTable"),
});
export const GristColumn = t.iface([], {
"id": "string",
"type": "string",
});
const exportedTypeSuite: t.ITypeSuite = {
GristTable,
GristTables,
GristColumn,
};
export default exportedTypeSuite;

38
app/plugin/GristTable.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* Common definitions for Grist plugin APIs.
*/
/**
*
* Metadata and data for a table. This is documenting what is currently returned by the
* core plugins. Could be worth reconciling with:
* https://phab.getgrist.com/w/grist_data_format/
* Capitalization is python-style.
*
*/
export interface GristTable {
table_name: string | null; // currently allow names to be null
column_metadata: GristColumn[];
table_data: any[][];
}
export interface GristTables {
tables: GristTable[];
}
/**
*
* Metadata about a single column.
*
*/
export interface GristColumn {
id: string;
type: string;
}
export enum APIType {
ImportSourceAPI,
ImportProcessorAPI,
ParseOptionsAPI,
ParseFileAPI,
}

View File

@@ -0,0 +1,44 @@
/**
* This module was automatically generated by `ts-interface-builder`
*/
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const ImportSourceAPI = t.iface([], {
"getImportSource": t.func(t.union("ImportSource", "undefined")),
});
export const ImportProcessorAPI = t.iface([], {
"processImport": t.func(t.array("GristTable"), t.param("source", "ImportSource")),
});
export const FileContent = t.iface([], {
"content": "any",
"name": "string",
});
export const FileListItem = t.iface([], {
"kind": t.lit("fileList"),
"files": t.array("FileContent"),
});
export const URL = t.iface([], {
"kind": t.lit("url"),
"url": "string",
});
export const ImportSource = t.iface([], {
"item": t.union("FileListItem", "URL"),
"options": t.opt(t.union("string", "Buffer")),
"description": t.opt("string"),
});
const exportedTypeSuite: t.ITypeSuite = {
ImportSourceAPI,
ImportProcessorAPI,
FileContent,
FileListItem,
URL,
ImportSource,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,53 @@
/**
* API definitions for ImportSource plugins.
*/
import { GristTable } from './GristTable';
export interface ImportSourceAPI {
/**
* Returns a promise that resolves to an `ImportSource` which is then passed for import to the
* import modal dialog. `undefined` interrupts the workflow and prevent the modal from showing up,
* but not an empty list of `ImportSourceItem`. Which is a valid import source and is used in
* cases where only options are to be sent to an `ImportProcessAPI` implementation.
*/
getImportSource(): Promise<ImportSource|undefined>;
}
export interface ImportProcessorAPI {
processImport(source: ImportSource): Promise<GristTable[]>;
}
export interface FileContent {
content: any;
name: string;
}
export interface FileListItem {
kind: "fileList";
// TODO: there're might be a better way to send file content. In particular for electron where
// file will then be send from client to server where it shouldn't be really. An idea could be to
// expose something similar to `client/lib/upload.ts` to let plugins create upload entries, and
// then send only uploads ids over the rpc.
files: FileContent[];
}
export interface URL {
kind: "url";
url: string;
}
export interface ImportSource {
item: FileListItem | URL;
/**
* The options are only passed within this plugin, nothing else needs to know how they are
* serialized. Using JSON.stringify/JSON.parse is a simple approach.
*/
options?: string|Buffer;
/**
* The short description that shows in the import dialog after source have been selected.
*/
description?: string;
}

View File

@@ -0,0 +1,11 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const InternalImportSourceAPI = t.iface([], {
"getImportSource": t.func(t.union("ImportSource", "undefined"), t.param("inlineTarget", "RenderTarget")),
});
const exportedTypeSuite: t.ITypeSuite = {
InternalImportSourceAPI,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,24 @@
import { RenderTarget } from './RenderOptions';
import { ImportSource } from './ImportSourceAPI';
export * from './ImportSourceAPI';
/**
* This internal interface is implemented by grist-plugin-api.ts to support
* `grist.addImporter(...)`. This is this interface that grist stubs to calls
* `ImportSourceAPI`. However, some of the complexity (ie: rendering targets) is hidden from the
* plugin author which implements directly the simpler `ImportSourceAPI`.
*
* Reason for this interface is because we want to have the `inlineTarget` parameter but we don't
* want plugin author to have it.
*/
export interface InternalImportSourceAPI {
/**
* The `inlineTarget` argument which will be passed to the implementation of this method, can be
* used as follow `grist.api.render('index.html', inlineTarget)` to embbed `index.html` in the
* import panel. Or it can be ignored and use `'fullscreen'` in-place. It is used in
* `grist.addImporter(...)` according to the value of the `mode` argument.
*/
getImportSource(inlineTarget: RenderTarget): Promise<ImportSource|undefined>;
}

View File

@@ -0,0 +1,60 @@
/**
* This module was automatically generated by `ts-interface-builder`
*/
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const PublishedPlugin = t.iface(["BarePlugin"], {
"name": "string",
"version": "string",
});
export const BarePlugin = t.iface([], {
"components": t.iface([], {
"safeBrowser": t.opt("string"),
"safePython": t.opt("string"),
"unsafeNode": t.opt("string"),
"deactivate": t.opt(t.iface([], {
"inactivitySec": t.opt("number"),
})),
}),
"contributions": t.iface([], {
"importSources": t.opt(t.array("ImportSource")),
"fileParsers": t.opt(t.array("FileParser")),
"customSections": t.opt(t.array("CustomSection")),
}),
"experimental": t.opt("boolean"),
});
export const ImportSource = t.iface([], {
"label": "string",
"importSource": "Implementation",
"importProcessor": t.opt("Implementation"),
});
export const FileParser = t.iface([], {
"fileExtensions": t.array("string"),
"editOptions": t.opt("Implementation"),
"parseFile": "Implementation",
});
export const CustomSection = t.iface([], {
"path": "string",
"name": "string",
});
export const Implementation = t.iface([], {
"component": t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode")),
"name": "string",
"path": t.opt("string"),
});
const exportedTypeSuite: t.ITypeSuite = {
PublishedPlugin,
BarePlugin,
ImportSource,
FileParser,
CustomSection,
Implementation,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,219 @@
/**
* 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 {
/**
* 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;
/**
* 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;
/**
* 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;
}

View File

@@ -0,0 +1,14 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const RenderTarget = t.union(t.lit("fullscreen"), "number");
export const RenderOptions = t.iface([], {
"height": t.opt("string"),
});
const exportedTypeSuite: t.ITypeSuite = {
RenderTarget,
RenderOptions,
};
export default exportedTypeSuite;

View File

@@ -0,0 +1,11 @@
/**
* Where to append the content that a plugin renders.
*/
export type RenderTarget = "fullscreen" | number;
/**
* Options for the `grist.render` function.
*/
export interface RenderOptions {
height?: string;
}

View File

@@ -0,0 +1,15 @@
import * as t from "ts-interface-checker";
// tslint:disable:object-literal-key-quotes
export const Storage = t.iface([], {
"getItem": t.func("any", t.param("key", "string")),
"hasItem": t.func("boolean", t.param("key", "string")),
"setItem": t.func("void", t.param("key", "string"), t.param("value", "any")),
"removeItem": t.func("void", t.param("key", "string")),
"clear": t.func("void"),
});
const exportedTypeSuite: t.ITypeSuite = {
Storage,
};
export default exportedTypeSuite;

8
app/plugin/StorageAPI.ts Normal file
View File

@@ -0,0 +1,8 @@
// subset of WebStorage API
export interface Storage {
getItem(key: string): any;
hasItem(key: string): boolean;
setItem(key: string, value: any): void;
removeItem(key: string): void;
clear(): void;
}

View File

@@ -0,0 +1,50 @@
import {createCheckers, ICheckerSuite} from 'ts-interface-checker';
import CustomSectionAPITI from './CustomSectionAPI-ti';
import FileParserAPITI from './FileParserAPI-ti';
import GristAPITI from './GristAPI-ti';
import GristTableTI from './GristTable-ti';
import ImportSourceAPITI from './ImportSourceAPI-ti';
import InternalImportSourceAPITI from './InternalImportSourceAPI-ti';
import RenderOptionsTI from './RenderOptions-ti';
import StorageAPITI from './StorageAPI-ti';
/**
* The ts-interface-checker type suites are all exported with the "TI" suffix.
*/
export {
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI};
const allTypes = [
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI];
function checkDuplicates(types: Array<{[key: string]: object}>) {
const seen = new Set<string>();
for (const t of types) {
for (const key of Object.keys(t)) {
if (seen.has(key)) { throw new Error(`TypeCheckers: Duplicate type name ${key}`); }
seen.add(key);
// Uncomment the line below to generate updated list of included types.
// console.log(`'${key}' |`);
}
}
}
checkDuplicates(allTypes);
/**
* We also create and export a global checker object that includes all of the types above.
*/
export const checkers = createCheckers(...allTypes) as (
// The following Pick typecast ensures that Typescript can only use correct properties of the
// checkers object (e.g. checkers.GristAPI will compile, but checkers.GristApi will not).
// TODO: The restrictive type of ICheckerSuite should be generated automatically. (Currently
// generated by commenting out console.log() in checkDuplicates() above.)
Pick<ICheckerSuite,
'CustomSectionAPI' | 'EditOptionsAPI' | 'ParseFileAPI' | 'ParseOptions' | 'ParseOptionSchema' |
'FileSource' | 'ParseFileResult' | 'ComponentKind' | 'GristAPI' | 'GristDocAPI' | 'GristTable' |
'GristTables' | 'GristColumn' | 'GristView' | 'ImportSourceAPI' | 'ImportProcessorAPI' | 'FileContent' |
'FileListItem' | 'URL' | 'ImportSource' | 'InternalImportSourceAPI' | 'RenderTarget' |
'RenderOptions' | 'Storage'
>);

View File

@@ -0,0 +1,135 @@
// Provide a way to acess grist for iframe, web worker (which runs the main safeBrowser script) and
// unsafeNode. WebView should work the same way as iframe, grist is exposed just the same way and
// necessary api is exposed using preload script. Here we bootstrap from channel capabilities to key
// parts of the grist API.
// For iframe (and webview):
// user will add '<script src="/grist-api.js"></script>' and get a window.grist
// For web worker:
// use will add `self.importScripts('/grist-api.js');`
// For node, user will do something like:
// const {grist} = require('grist-api');
// grist.registerFunction();
// In TypeScript:
// import {grist} from 'grist-api';
// grist.registerFunction();
// tslint:disable:no-console
import { GristAPI, GristDocAPI, GristView, RPC_GRISTAPI_INTERFACE } from './GristAPI';
import { ImportSource, ImportSourceAPI, InternalImportSourceAPI } from './InternalImportSourceAPI';
import { RenderOptions, RenderTarget } from './RenderOptions';
import { checkers } from './TypeCheckers';
export * from './TypeCheckers';
export * from './FileParserAPI';
export * from './GristAPI';
export * from './GristTable';
export * from './ImportSourceAPI';
export * from './StorageAPI';
export * from './RenderOptions';
import {IRpcLogger, Rpc} from 'grain-rpc';
export const rpc: Rpc = new Rpc({logger: createRpcLogger()});
export const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI);
export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers.GristDocAPI);
export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView);
export const docApi = {
...coreDocApi,
...viewApi,
};
export const on = rpc.on.bind(rpc);
/**
* Calling `addImporter(...)` adds a safeBrowser importer. It is a short-hand for forwarding calls
* to an `ImportSourceAPI` implementation registered in the file at `path`. It takes care of
* creating the stub, registering an implementation that renders the file, forward the call and
* dispose the view properly. If `mode` is `'inline'` embeds the view in the import modal, ohterwise
* renders fullscreen.
*
* Notes: it assumes that file at `path` registers an `ImportSourceAPI` implementation under
* `name`. Calling `addImporter(...)` from another component than a `safeBrowser` component is not
* currently supported.
*
*/
export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) {
// checker is omitterd for implementation because call was alredy checked by grist.
rpc.registerImpl<InternalImportSourceAPI>(name, {
async getImportSource(target: RenderTarget): Promise<ImportSource|undefined> {
const procId = await api.render(path, mode === 'inline' ? target : 'fullscreen', options);
try {
// stubName for the interface `name` at forward destination `path`
const stubName = `${name}@${path}`;
// checker is omitted in stub because call will be checked just after in grist.
return await rpc.getStub<ImportSourceAPI>(stubName).getImportSource();
} finally {
await api.dispose(procId);
}
}
});
}
/**
* Declare that a component is prepared to receive messages from the outside world.
* Grist will not attempt to communicate with it until this method is called.
*/
export function ready(): void {
rpc.processIncoming();
rpc.sendReadyMessage();
}
function getPluginPath(location: Location) {
return location.pathname.replace(/^\/plugins\//, '');
}
if (typeof window !== 'undefined') {
// Window or iframe.
const preloadWindow: any = window;
if (preloadWindow.isRunningUnderElectron) {
rpc.setSendMessage(msg => preloadWindow.sendToHost(msg));
preloadWindow.onGristMessage((data: any) => rpc.receiveMessage(data));
} else {
rpc.setSendMessage(msg => window.parent.postMessage(msg, "*"));
window.onmessage = (e: MessageEvent) => rpc.receiveMessage(e.data);
}
} else if (typeof process === 'undefined') {
// Web worker. We can't really bring in the types for WebWorker (available with --lib flag)
// without conflicting with a regular window, so use just use `self as any` here.
self.onmessage = (e: MessageEvent) => rpc.receiveMessage(e.data);
rpc.setSendMessage((mssg: any) => (self as any).postMessage(mssg));
} else if (typeof process.send !== 'undefined') {
// Forked ChildProcess of node or electron.
// sendMessage callback returns void 0 because rpc process.send returns a boolean and rpc
// expecting void|Promise interprets truthy values as Promise which cause failure.
rpc.setSendMessage((data) => { process.send!(data); });
process.on('message', (data: any) => rpc.receiveMessage(data));
process.on('disconnect', () => { process.exit(0); });
} else {
// Not a recognized environment, perhaps plain nodejs run independently of Grist, or tests
// running under mocha. For now, we only provide a disfunctional implementation. It allows
// plugins to call methods like registerFunction() without failing, so that plugin code may be
// imported, but the methods don't do anything useful.
rpc.setSendMessage((data) => {return; });
}
function createRpcLogger(): IRpcLogger {
let prefix: string;
if (typeof window !== 'undefined') {
prefix = `PLUGIN VIEW ${getPluginPath(window.location)}:`;
} else if (typeof process === 'undefined') {
prefix = `PLUGIN VIEW ${getPluginPath(self.location)}:`;
} else if (typeof process.send !== 'undefined') {
prefix = `PLUGIN NODE ${process.env.GRIST_PLUGIN_PATH || "<unset-plugin-id>"}:`;
} else {
return {};
}
return {
info(msg: string) { console.log("%s %s", prefix, msg); },
warn(msg: string) { console.warn("%s %s", prefix, msg); },
};
}

3
app/plugin/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../../buildtools/tsconfig-base.json",
}