(core) Speed up and upgrade build.

Summary:
- Upgrades to build-related packages:
  - Upgrade typescript, related libraries and typings.
  - Upgrade webpack, eslint; add tsc-watch, node-dev, eslint_d.

- Build organization changes:
  - Build webpack from original typescript, transpiling only; with errors still
    reported by a background tsc watching process.

- Typescript-related changes:
  - Reduce imports of AWS dependencies (very noticeable speedup)
  - Avoid auto-loading global @types
  - Client code is now built with isolatedModules flag (for safe transpilation)
  - Use allowJs to avoid copying JS files manually.

- Linting changes
  - Enhance Arcanist ESLintLinter to run before/after commands, and set up to use eslint_d
  - Update eslint config, and include .eslintignore to avoid linting generated files.
  - Include a bunch of eslint-prompted and eslint-generated fixes
  - Add no-unused-expression rule to eslint, and fix a few warnings about it

- Other items:
  - Refactor cssInput to avoid circular dependency
  - Remove a bit of unused code, libraries, dependencies

Test Plan: No behavior changes, all existing tests pass. There are 30 tests fewer reported because `test_gpath.py` was removed (it's been unused for years)

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D3498
This commit is contained in:
Dmitry S
2022-06-27 16:09:41 -04:00
parent 64ff9ccd0a
commit dd2eadc86e
45 changed files with 948 additions and 2442 deletions

View File

@@ -81,7 +81,7 @@ export class Cursor extends Disposable {
this.viewData = baseView.viewData;
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()));
this._rowId = ko.observable(optCursorPos.rowId || 0);
this._rowId = ko.observable<RowId|null>(optCursorPos.rowId || 0);
this.rowIndex = this.autoDispose(ko.computed({
read: () => {
if (!this._isLive()) { return this.rowIndex.peek(); }

View File

@@ -93,7 +93,7 @@ export class TypeTransform extends ColumnTransform {
const colInfo = await TypeConversion.prepTransformColInfo(docModel, this.origColumn, this.origDisplayCol, toType);
// NOTE: We could add rules with AddColumn action, but there are some optimizations that converts array values.
const rules = colInfo.rules;
delete colInfo.rules;
delete (colInfo as any).rules;
const newColInfos = await this._tableData.sendTableActions([
['AddColumn', 'gristHelper_Converted', {...colInfo, isFormula: false, formula: ''}],
['AddColumn', 'gristHelper_Transform', colInfo],

View File

@@ -1,6 +1,7 @@
import { GristDoc } from 'app/client/components/GristDoc';
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
import { cssField, cssInput, cssLabel} from 'app/client/ui/MakeCopyMenu';
import { cssInput } from 'app/client/ui/cssInput';
import { cssField, cssLabel } from 'app/client/ui/MakeCopyMenu';
import { IPageWidget, toPageWidget } from 'app/client/ui/PageWidgetPicker';
import { confirmModal } from 'app/client/ui2018/modals';
import { BulkColValues, getColValues, RowRecord, UserAction } from 'app/common/DocActions';

View File

@@ -12,7 +12,6 @@ declare module "app/client/lib/browserGlobals";
declare module "app/client/lib/dom";
declare module "app/client/lib/koDom";
declare module "app/client/lib/koForm";
declare module "app/client/lib/koSession";
declare module "app/client/widgets/UserType";
declare module "app/client/widgets/UserTypeImpl";
@@ -319,3 +318,9 @@ declare module "app/client/lib/koUtil" {
// with polyfills for old browsers.
declare module "bowser/bundled";
declare module "randomcolor";
interface Location {
// We use reload(true) in places, which has an effect in Firefox, but may be more of a
// historical accident than an intentional choice.
reload(forceGet?: boolean): void;
}

View File

@@ -8,7 +8,9 @@
exports.loadBillingPage = () => import('app/client/ui/BillingPage' /* webpackChunkName: "BillingModule" */);
exports.loadGristDoc = () => import('app/client/components/GristDoc' /* webpackChunkName: "GristDoc" */);
exports.loadMomentTimezone = () => import('moment-timezone');
// When importing this way, the module is under the "default" member, not sure why (maybe
// esbuild-loader's doing).
exports.loadMomentTimezone = () => import('moment-timezone').then(m => m.default);
exports.loadPlotly = () => import('plotly.js-basic-dist' /* webpackChunkName: "plotly" */);
exports.loadSearch = () => import('app/client/ui2018/search' /* webpackChunkName: "search" */);
exports.loadUserManager = () => import('app/client/ui/UserManager' /* webpackChunkName: "usermanager" */);

View File

@@ -1,90 +0,0 @@
/**
* koSession offers observables whose values are tied to the browser session or history:
*
* sessionValue(key) - an observable preserved across history entries and reloads.
*
* Note: we could also support "browserValue", shared across all tabs and across browser restarts
* (same as sessionValue but using window.localStorage), but it seems more appropriate to store
* such values on the server.
*/
/* global window, $ */
var _ = require('underscore');
var ko = require('knockout');
/**
* Maps a string key to an observable. The space of keys is shared for all kinds of observables,
* and they differ only in where they store their state. Each observable gets several extra
* properties:
* @property {String} ksKey The key used for storage. It should be unique across koSession values.
* @property {Object} ksDefault The default value if the storage doesn't have one.
* @property {Function} ksFetch The method to fetch the value from storage.
* @property {Function} ksSave The method to save the value to storage.
*/
var _sessionValues = {};
function createObservable(key, defaultValue, methods) {
var obs = _sessionValues[key];
if (!obs) {
_sessionValues[key] = obs = ko.observable();
obs.ksKey = key;
obs.ksDefaultValue = defaultValue;
obs.ksFetch = methods.fetch;
obs.ksSave = methods.save;
obs.dispose = methods.dispose;
// We initialize the observable before setting rateLimit, to ensure that the initialization
// doesn't end up triggering subscribers that are about to be added (which seems to be a bit
// of a problem with rateLimit extender, and possibly deferred). This workaround relies on the
// fact that the extender modifies its target without creating a new one.
obs(obs.ksFetch());
obs.extend({deferred: true});
obs.subscribe(function(newValue) {
if (newValue !== this.ksFetch()) {
console.log("koSession: %s changed %s -> %s", this.ksKey, this.ksFetch(), newValue);
this.ksSave(newValue);
}
}, obs);
}
return obs;
}
/**
* Returns an observable whose value sticks across reloads and navigation, but is different for
* different browser tabs. E.g. it may be used to reflect whether a side pane is open.
* The `key` isn't visible to the user, so pick any unique string name.
*/
function sessionValue(key, optDefault) {
return createObservable(key, optDefault, sessionValueMethods);
}
exports.sessionValue = sessionValue;
var sessionValueMethods = {
'fetch': function() {
var value = window.sessionStorage.getItem(this.ksKey);
if (!value) {
return this.ksDefaultValue;
}
try {
return JSON.parse(value);
} catch (e) {
return this.ksDefaultValue;
}
},
'save': function(value) {
window.sessionStorage.setItem(this.ksKey, JSON.stringify(value));
},
'dispose': function(value) {
window.sessionStorage.removeItem(this.ksKey);
}
};
function onApplyState() {
_.each(_sessionValues, function(obs, key) {
obs(obs.ksFetch());
});
}
$(window).on('applyState', onApplyState);

View File

@@ -21,13 +21,12 @@ export class DataRowModel extends BaseRowModel {
public _validationFailures: ko.PureComputed<Array<IRowModel<'_grist_Validations'>>>;
public _isAddRow: ko.Observable<boolean>;
private _allValidationsList: ko.Computed<KoArray<ValidationRec>>;
private _isRealChange: ko.Observable<boolean>;
public constructor(dataTableModel: DataTableModel, colNames: string[]) {
super(dataTableModel, colNames);
this._allValidationsList = dataTableModel.tableMetaRow.validations;
const allValidationsList: ko.Computed<KoArray<ValidationRec>> = dataTableModel.tableMetaRow.validations;
this._isAddRow = ko.observable(false);
@@ -36,10 +35,10 @@ export class DataRowModel extends BaseRowModel {
// changes, those should only be enabled when _isRealChange is true.
this._isRealChange = ko.observable(true);
this._validationFailures = this.autoDispose(ko.pureComputed(function() {
return this._allValidationsList().all().filter(
this._validationFailures = this.autoDispose(ko.pureComputed(() => {
return allValidationsList().all().filter(
validation => !this.cells[this.getValidationNameFromId(validation.id())]());
}, this));
}));
}
/**

View File

@@ -41,16 +41,8 @@ import {decodeObject} from 'app/plugin/objtypes';
// Re-export all the entity types available. The recommended usage is like this:
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
export {ColumnRec} from 'app/client/models/entities/ColumnRec';
export {DocInfoRec} from 'app/client/models/entities/DocInfoRec';
export {FilterRec} from 'app/client/models/entities/FilterRec';
export {PageRec} from 'app/client/models/entities/PageRec';
export {TabBarRec} from 'app/client/models/entities/TabBarRec';
export {TableRec} from 'app/client/models/entities/TableRec';
export {ValidationRec} from 'app/client/models/entities/ValidationRec';
export {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
export {ViewRec} from 'app/client/models/entities/ViewRec';
export {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
export type {ColumnRec, DocInfoRec, FilterRec, PageRec, TabBarRec, TableRec, ValidationRec,
ViewFieldRec, ViewRec, ViewSectionRec};
/**

View File

@@ -6,7 +6,7 @@ import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
export {ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
export type {ColumnFilterFunc};
interface OpenColumnFilter {
colRef: number;

View File

@@ -306,7 +306,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// in which case the UI prevents various things like hiding columns or changing the widget type.
this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId()));
this.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this);
this.borderWidthPx = ko.pureComputed(() => this.borderWidth() + 'px');
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
@@ -487,7 +487,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
this.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef);
this.activeRowId = ko.observable(null);
this.activeRowId = ko.observable<RowId|null>(null);
this._linkingState = Holder.create(this);
this.linkingState = this.autoDispose(ko.pureComputed(() => {
@@ -506,7 +506,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
}));
// If the view instance for this section is instantiated, it will be accessible here.
this.viewInstance = ko.observable(null);
this.viewInstance = ko.observable<BaseView|null>(null);
// Describes the most recent cursor position in the section.
this.lastCursorPos = {
@@ -542,8 +542,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
);
this.hasCustomOptions = ko.observable(false);
this.desiredAccessLevel = ko.observable(null);
this.columnsToMap = ko.observable(null);
this.desiredAccessLevel = ko.observable<AccessLevel|null>(null);
this.columnsToMap = ko.observable<ColumnsToMap|null>(null);
// Calculate mapped columns for Custom Widget.
this.mappedColumns = ko.pureComputed(() => {
// First check if widget has requested a custom column mapping and

View File

@@ -236,7 +236,7 @@ export class BillingPage extends Disposable {
moneyPlan?.amount ? [
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
` member${sub.userCount !== 1 ? 's' : ''}`]),
tier ? this._makeAppSumoFeature(discountName!) : null,
tier ? this._makeAppSumoFeature(discountName) : null,
// Currently the subtotal is misleading and scary when tiers are in effect.
// In this case, for now, just report what will be invoiced.
!tier ? makeSummaryFeature([`Your ${moneyPlan.interval}ly subtotal is `,

View File

@@ -6,6 +6,7 @@
import {AppModel, reportError} from 'app/client/models/AppModel';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo';
import {cssInput} from 'app/client/ui/cssInput';
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
@@ -275,16 +276,6 @@ class SaveCopyModal extends Disposable {
}
}
export const cssInput = styled('input', `
height: 30px;
width: 100%;
font-size: ${vars.mediumFontSize};
border-radius: 3px;
padding: 5px;
border: 1px solid ${colors.darkGrey};
outline: none;
`);
export const cssField = styled('div', `
margin: 16px 0;
display: flex;

12
app/client/ui/cssInput.ts Normal file
View File

@@ -0,0 +1,12 @@
import {colors, vars} from 'app/client/ui2018/cssVars';
import {styled} from 'grainjs';
export const cssInput = styled('input', `
height: 30px;
width: 100%;
font-size: ${vars.mediumFontSize};
border-radius: 3px;
padding: 5px;
border: 1px solid ${colors.darkGrey};
outline: none;
`);

View File

@@ -75,7 +75,7 @@ export function prepareForTransition(elem: HTMLElement, prepare: () => void) {
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow.
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
window.getComputedStyle(elem).opacity; // eslint-disable-line no-unused-expressions
// Restore transitions.
elem.style.transitionProperty = prior;

View File

@@ -1,6 +1,6 @@
import {FocusLayer} from 'app/client/lib/FocusLayer';
import {reportError} from 'app/client/models/errors';
import {cssInput} from 'app/client/ui/MakeCopyMenu';
import {cssInput} from 'app/client/ui/cssInput';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';

View File

@@ -29,7 +29,7 @@ export class ChoiceTextBox extends NTextBox {
private _choiceValues: Computed<string[]>;
private _choiceValuesSet: Computed<Set<string>>;
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
private _choiceOptionsByName: Computed<ChoiceOptionsByName>;
constructor(field: ViewFieldRec) {
super(field);

View File

@@ -17,6 +17,9 @@ export interface IMargins {
right: number;
}
export type IRect = ISize & IMargins;
// edgeMargin is how many pixels to leave before the edge of the browser window by default.
// This is added to margins that may be passed into the constructor.
const edgeMargin = 12;
@@ -37,8 +40,8 @@ export class EditorPlacement extends Disposable {
public readonly onReposition = this.autoDispose(new Emitter());
private _editorRoot: HTMLElement;
private _maxRect: ClientRect|DOMRect;
private _cellRect: ClientRect|DOMRect;
private _maxRect: IRect;
private _cellRect: IRect;
private _margins: IMargins;
// - editorDom is the DOM to attach. It gets destroyed when EditorPlacement is disposed.
@@ -141,7 +144,7 @@ export class EditorPlacement extends Disposable {
// Get the bounding rect of elem excluding borders. This allows the editor to match cellElem more
// closely which is more visible in case of DetailView.
function rectWithoutBorders(elem: Element): ClientRect {
function rectWithoutBorders(elem: Element): IRect {
const rect = elem.getBoundingClientRect();
const style = getComputedStyle(elem, null);
const bTop = parseFloat(style.getPropertyValue('border-top-width'));

View File

@@ -55,6 +55,7 @@ function getTypeDefinition(type: string | false) {
}
type ComputedStyle = {style?: Style; error?: true} | null | undefined;
/**
* Builds a font option computed property.
*/
@@ -62,12 +63,13 @@ function buildFontOptions(
builder: FieldBuilder,
computedRule: ko.Computed<ComputedStyle>,
optionName: keyof Style) {
return koUtil.withKoUtils(ko.computed(function() {
return koUtil.withKoUtils(ko.computed(() => {
if (builder.isDisposed()) { return false; }
const style = computedRule()?.style;
const styleFlag = style?.[optionName] || this.field[optionName]();
const styleFlag = style?.[optionName] || builder.field[optionName]();
return styleFlag;
}, builder)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
}
/**
@@ -131,9 +133,9 @@ export class FieldBuilder extends Disposable {
});
// Observable which evaluates to a *function* that decides if a value is valid.
this._isRightType = ko.pureComputed(function() {
this._isRightType = ko.pureComputed<(value: CellValue, options?: any) => boolean>(() => {
return gristTypes.isRightType(this._readOnlyPureType()) || _.constant(false);
}, this);
});
// Returns a boolean indicating whether the column is type Reference or ReferenceList.
this._isRef = this.autoDispose(ko.computed(() => {
@@ -154,7 +156,7 @@ export class FieldBuilder extends Disposable {
}
}));
this.widget = ko.pureComputed({
this.widget = ko.pureComputed<object>({
owner: this,
read() { return this.options().widget; },
write(widget) {
@@ -196,10 +198,10 @@ export class FieldBuilder extends Disposable {
this._rowMap = new Map();
// Returns the constructor for the widget, and only notifies subscribers on changes.
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(function() {
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(() => {
return UserTypeImpl.getWidgetConstructor(this.options().widget,
this._readOnlyPureType());
}, this)).onlyNotifyUnequal());
})).onlyNotifyUnequal());
// Computed builder for the widget.
this.widgetImpl = this.autoDispose(koUtil.computedBuilder(() => {
@@ -467,28 +469,28 @@ export class FieldBuilder extends Disposable {
return { style : new CombinedStyle(styles, flags) };
}, this).extend({ deferred: true })).previousOnUndefined();
const widgetObs = koUtil.withKoUtils(ko.computed(function() {
const widgetObs = koUtil.withKoUtils(ko.computed(() => {
// TODO: Accessing row values like this doesn't always work (row and field might not be updated
// simultaneously).
if (this.isDisposed()) { return null; } // Work around JS errors during field removal.
const value = row.cells[this.field.colId()];
const cell = value && value();
if (value && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
if ((value) && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
return this.widgetImpl();
} else if (gristTypes.isVersions(cell)) {
return this.diffImpl;
} else {
return null;
}
}, this).extend({ deferred: true })).onlyNotifyUnequal();
}).extend({ deferred: true })).onlyNotifyUnequal();
const textColor = koUtil.withKoUtils(ko.computed(function() {
const textColor = koUtil.withKoUtils(ko.computed(() => {
if (this.isDisposed()) { return null; }
const fromRules = computedRule()?.style?.textColor;
return fromRules || this.field.textColor() || '';
}, this)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
const fillColor = koUtil.withKoUtils(ko.computed(function() {
const fillColor = koUtil.withKoUtils(ko.computed(() => {
if (this.isDisposed()) { return null; }
const fromRules = computedRule()?.style?.fillColor;
let fill = fromRules || this.field.fillColor();
@@ -496,7 +498,7 @@ export class FieldBuilder extends Disposable {
// If there is no color we are using fully transparent white color (for tests mainly).
fill = fill ? fill.toUpperCase() : fill;
return (fill === '#FFFFFF' ? '' : fill) || '#FFFFFF00';
}, this)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
const fontBold = buildFontOptions(this, computedRule, 'fontBold');
const fontItalic = buildFontOptions(this, computedRule, 'fontItalic');

View File

@@ -135,6 +135,7 @@ export class NTextEditor extends NewBaseEditor {
// but we got same enough spaces, we will force browser to check the available space once more time.
if (enoughSpace(rect, size) && hasScroll(textInput)) {
textInput.style.overflow = "hidden";
// eslint-disable-next-line no-unused-expressions
textInput.clientHeight; // just access metrics is enough to repaint
textInput.style.overflow = "auto";
}

View File

@@ -14,6 +14,7 @@ import {
dom,
DomContents,
fromKo,
IDisposableOwnerT,
Observable,
} from 'grainjs';
@@ -30,8 +31,13 @@ export abstract class NewAbstractWidget extends Disposable {
/**
* Override the create() method to match the parameters of create() expected by FieldBuilder.
*/
public static create(field: ViewFieldRec) {
return Disposable.create.call(this as any, null, field);
// We copy Disposable.create() signature (the second one) to pacify typescript, but code must
// use the first signature, which is compatible with old-style constructors.
public static create<T extends new (...args: any[]) => any>(field: ViewFieldRec): InstanceType<T>;
public static create<T extends new (...args: any[]) => any>(
this: T, owner: IDisposableOwnerT<InstanceType<T>>|null, ...args: ConstructorParameters<T>): InstanceType<T>;
public static create(...args: any[]) {
return Disposable.create.call(this as any, null, ...args);
}
protected options: SaveableObjObservable<any>;

View File

@@ -4,7 +4,7 @@
// Some definitions have moved to be part of plugin API.
import { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
export { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
export type { BulkColValues, CellValue, RowRecord };
// Part of a special CellValue used for comparisons, embedding several versions of a CellValue.
export interface AllCellVersions {
@@ -173,7 +173,7 @@ export function getNumRows(action: DocAction): number {
export function toTableDataAction(tableId: string, colValues: TableColValues): TableDataAction {
const colData = {...colValues}; // Make a copy to avoid changing passed-in arguments.
const rowIds: number[] = colData.id;
delete colData.id;
delete (colData as BulkColValues).id;
return ['TableData', tableId, rowIds, colData];
}

View File

@@ -67,7 +67,7 @@ export type AclMatchFunc = (input: AclMatchInput) => boolean;
* Representation of a parsed ACL formula.
*/
type PrimitiveCellValue = number|string|boolean|null;
export type ParsedAclFormula = [string, ...Array<ParsedAclFormula|PrimitiveCellValue>];
export type ParsedAclFormula = [string, ...(ParsedAclFormula|PrimitiveCellValue)[]];
/**
* Observations about a formula.

View File

@@ -13,9 +13,6 @@
*
*/
// important to explicitly import this, or webpack --watch gets confused.
import {clearTimeout, setTimeout} from "timers";
export class InactivityTimer {
private _timeout?: NodeJS.Timer | null;

View File

@@ -19,7 +19,7 @@ try {
const display = (code: string) => {
try {
const locale = new Intl.Locale(code);
const regionName = regionDisplay.of(locale.region);
const regionName = regionDisplay.of(locale.region!);
const languageName = languageDisplay.of(locale.language);
return `${regionName} (${languageName})`;
} catch (ex) {
@@ -58,7 +58,7 @@ export function getCurrency(code: string) {
try {
const currencyDisplay = new Intl.DisplayNames('en', {type: 'currency'});
currencies = [...new Set(currenciesCodes)].map(code => {
return {name: currencyDisplay.of(code), code};
return {name: currencyDisplay.of(code)!, code};
});
} catch {
// Fall back to using the currency code as the display name.

View File

@@ -255,10 +255,6 @@ export class HomeDBManager extends EventEmitter {
// In restricted mode, documents should be read-only.
private _restrictedMode: boolean = false;
public emit(event: NotifierEvent, ...args: any[]): boolean {
return super.emit(event, ...args);
}
/**
* Five aclRules, each with one group (with the names 'owners', 'editors', 'viewers',
* 'guests', and 'members') are created by default on every new entity (Organization,
@@ -298,6 +294,10 @@ export class HomeDBManager extends EventEmitter {
orgOnly: true
}];
public emit(event: NotifierEvent, ...args: any[]): boolean {
return super.emit(event, ...args);
}
// All groups.
public get defaultGroups(): GroupDescriptor[] {
return this._defaultGroups;

View File

@@ -1,5 +1,5 @@
// Letter codes for CellValue types encoded as [code, args...] tuples.
export const enum GristObjCode {
export enum GristObjCode {
List = 'L',
LookUp = 'l',
Dict = 'O',

View File

@@ -1,4 +1,4 @@
import {createCheckers, ICheckerSuite} from 'ts-interface-checker';
import {BasicType, createCheckers, ICheckerSuite} from 'ts-interface-checker';
import CustomSectionAPITI from './CustomSectionAPI-ti';
import FileParserAPITI from './FileParserAPI-ti';
import GristAPITI from './GristAPI-ti';
@@ -18,7 +18,14 @@ export {
const allTypes = [
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI];
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI,
];
// Ensure Buffer can be handled if mentioned in the interface descriptions, even if not supported
// in the current environment (i.e. browser).
if (typeof Buffer === 'undefined') {
allTypes.push({Buffer: new BasicType((v) => false, "Buffer is not supported")});
}
function checkDuplicates(types: Array<{[key: string]: object}>) {
const seen = new Set<string>();

View File

@@ -402,7 +402,7 @@ export interface ReadyPayload extends Omit<InteractionOptionsRequest, "hasCustom
/**
* Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.
*/
onEditOptions: () => unknown;
onEditOptions?: () => unknown;
}
/**
* Declare that a component is prepared to receive messages from the outside world.

View File

@@ -18,33 +18,6 @@ declare module "bluebird" {
class Disposer<T> {}
}
// TODO This is a module by Grist Labs; we should add index.d.ts to it.
declare module "@gristlabs/basket-api" {
interface Item { [colId: string]: any; }
interface ColValues { [colId: string]: any[]; }
interface AuthToken { [authProvider: string]: string; }
class Basket {
public static addBasket(login: AuthToken): Promise<string>;
public static getBaskets(login: AuthToken): Promise<string[]>;
public basketId: Readonly<string>;
public apiKey: Readonly<string|undefined>;
constructor(basketId: string, apiKey?: string);
public addTable(optTableId: string): Promise<string>;
public getTable(tableId: string): Promise<Item[]>;
public renameTable(oldTableId: string, newTableId: string): Promise<void>;
public replaceTableData(tableId: string, columnValues: ColValues): Promise<void>;
public deleteTable(tableId: string): Promise<void>;
public getTables(): Promise<string[]>;
public uploadAttachment(attachmentId: string, attachment: Buffer): Promise<void>;
public delete(login: AuthToken): Promise<void>;
}
namespace Basket {}
export = Basket;
}
// Used in one place, and the typings are almost entirely unhelpful.
declare module "multiparty";

View File

@@ -49,7 +49,7 @@ import {
adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getOriginUrl, getScope, optStringParam,
RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
import {getDatabaseUrl} from 'app/server/lib/serverUtils';
import {getDatabaseUrl, listenPromise} from 'app/server/lib/serverUtils';
import {Sessions} from 'app/server/lib/Sessions';
import * as shutdown from 'app/server/lib/shutdown';
import {TagChecker} from 'app/server/lib/TagChecker';
@@ -1666,14 +1666,11 @@ export class FlexServer implements GristServer {
private async _startServers(server: http.Server, httpsServer: https.Server|undefined,
name: string, port: number, verbose: boolean) {
await new Promise((resolve, reject) => server.listen(port, this.host, resolve).on('error', reject));
await listenPromise(server.listen(port, this.host));
if (verbose) { log.info(`${name} available at ${this.host}:${port}`); }
if (TEST_HTTPS_OFFSET && httpsServer) {
const httpsPort = port + TEST_HTTPS_OFFSET;
await new Promise((resolve, reject) => {
httpsServer.listen(httpsPort, this.host, resolve)
.on('error', reject);
});
await listenPromise(httpsServer.listen(httpsPort, this.host));
if (verbose) { log.info(`${name} available at https://${this.host}:${httpsPort}`); }
}
}

View File

@@ -1,5 +1,6 @@
export interface INotifier {
deleteUser(userId: number): Promise<void>;
// for test purposes, check if any notifications are in progress
readonly testPending: boolean;
deleteUser(userId: number): Promise<void>;
}

View File

@@ -125,16 +125,16 @@ export class NSandbox implements ISandbox {
if (options.minimalPipeMode) {
log.rawDebug("3-pipe Sandbox started", this._logMeta);
this._streamToSandbox = this.childProc.stdin;
this._streamFromSandbox = this.childProc.stdout;
this._streamToSandbox = this.childProc.stdin!;
this._streamFromSandbox = this.childProc.stdout!;
} else {
log.rawDebug("5-pipe Sandbox started", this._logMeta);
this._streamToSandbox = (this.childProc.stdio as Stream[])[3] as Writable;
this._streamFromSandbox = (this.childProc.stdio as Stream[])[4];
this.childProc.stdout.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
this.childProc.stdout!.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
}
const sandboxStderrLogger = sandboxUtil.makeLinePrefixer('Sandbox stderr: ', this._logMeta);
this.childProc.stderr.on('data', data => {
this.childProc.stderr!.on('data', data => {
this._lastStderr = data;
sandboxStderrLogger(data);
});

View File

@@ -16,7 +16,7 @@ import {IMsgCustom, IMsgRpcCall} from 'grain-rpc';
*/
export class SafePythonComponent extends BaseComponent {
private _sandbox: ISandbox;
private _sandbox?: ISandbox;
private _logMeta: log.ILogMeta;
// safe python component does not need pluginInstance.rpc because it is not possible to forward

View File

@@ -118,7 +118,7 @@ export class DocTriggers {
public shutdown() {
this._shuttingDown = true;
if (!this._sending) {
this._redisClientField?.quitAsync();
void(this._redisClientField?.quitAsync());
}
}

View File

@@ -121,8 +121,8 @@ export class UnsafeNodeComponent extends BaseComponent {
.catch(err => log.warn("unsafeNode[%s] failed with %s", child.pid, err))
.then(() => { this._child = undefined; });
child.stdout.on('data', makeLinePrefixer('PLUGIN stdout: '));
child.stderr.on('data', makeLinePrefixer('PLUGIN stderr: '));
child.stdout!.on('data', makeLinePrefixer('PLUGIN stdout: '));
child.stderr!.on('data', makeLinePrefixer('PLUGIN stderr: '));
warnIfNotReady(this._rpc, 3000, "Plugin isn't ready; be sure to call grist.ready() from plugin");
child.on('message', this._rpc.receiveMessage.bind(this._rpc));

View File

@@ -63,6 +63,13 @@ export function connect(arg: any, ...moreArgs: any[]): Promise<net.Socket> {
});
}
/**
* Promisified version of net.Server.listen().
*/
export function listenPromise<T extends net.Server>(server: T): Promise<void> {
return new Promise<void>((resolve, reject) => server.once('listening', resolve).once('error', reject));
}
/**
* Returns whether the path `inner` is contained within the directory `outer`.
*/