(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
pull/214/head
Dmitry S 2 years ago
parent 64ff9ccd0a
commit dd2eadc86e

@ -81,7 +81,7 @@ export class Cursor extends Disposable {
this.viewData = baseView.viewData; this.viewData = baseView.viewData;
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id())); 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({ this.rowIndex = this.autoDispose(ko.computed({
read: () => { read: () => {
if (!this._isLive()) { return this.rowIndex.peek(); } if (!this._isLive()) { return this.rowIndex.peek(); }

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

@ -1,6 +1,7 @@
import { GristDoc } from 'app/client/components/GristDoc'; import { GristDoc } from 'app/client/components/GristDoc';
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel'; 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 { IPageWidget, toPageWidget } from 'app/client/ui/PageWidgetPicker';
import { confirmModal } from 'app/client/ui2018/modals'; import { confirmModal } from 'app/client/ui2018/modals';
import { BulkColValues, getColValues, RowRecord, UserAction } from 'app/common/DocActions'; import { BulkColValues, getColValues, RowRecord, UserAction } from 'app/common/DocActions';

@ -12,7 +12,6 @@ declare module "app/client/lib/browserGlobals";
declare module "app/client/lib/dom"; declare module "app/client/lib/dom";
declare module "app/client/lib/koDom"; declare module "app/client/lib/koDom";
declare module "app/client/lib/koForm"; declare module "app/client/lib/koForm";
declare module "app/client/lib/koSession";
declare module "app/client/widgets/UserType"; declare module "app/client/widgets/UserType";
declare module "app/client/widgets/UserTypeImpl"; declare module "app/client/widgets/UserTypeImpl";
@ -319,3 +318,9 @@ declare module "app/client/lib/koUtil" {
// with polyfills for old browsers. // with polyfills for old browsers.
declare module "bowser/bundled"; declare module "bowser/bundled";
declare module "randomcolor"; 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;
}

@ -8,7 +8,9 @@
exports.loadBillingPage = () => import('app/client/ui/BillingPage' /* webpackChunkName: "BillingModule" */); exports.loadBillingPage = () => import('app/client/ui/BillingPage' /* webpackChunkName: "BillingModule" */);
exports.loadGristDoc = () => import('app/client/components/GristDoc' /* webpackChunkName: "GristDoc" */); 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.loadPlotly = () => import('plotly.js-basic-dist' /* webpackChunkName: "plotly" */);
exports.loadSearch = () => import('app/client/ui2018/search' /* webpackChunkName: "search" */); exports.loadSearch = () => import('app/client/ui2018/search' /* webpackChunkName: "search" */);
exports.loadUserManager = () => import('app/client/ui/UserManager' /* webpackChunkName: "usermanager" */); exports.loadUserManager = () => import('app/client/ui/UserManager' /* webpackChunkName: "usermanager" */);

@ -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);

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

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

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

@ -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. // 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.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); 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.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef); 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 = Holder.create(this);
this.linkingState = this.autoDispose(ko.pureComputed(() => { 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. // 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. // Describes the most recent cursor position in the section.
this.lastCursorPos = { this.lastCursorPos = {
@ -542,8 +542,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
); );
this.hasCustomOptions = ko.observable(false); this.hasCustomOptions = ko.observable(false);
this.desiredAccessLevel = ko.observable(null); this.desiredAccessLevel = ko.observable<AccessLevel|null>(null);
this.columnsToMap = ko.observable(null); this.columnsToMap = ko.observable<ColumnsToMap|null>(null);
// Calculate mapped columns for Custom Widget. // Calculate mapped columns for Custom Widget.
this.mappedColumns = ko.pureComputed(() => { this.mappedColumns = ko.pureComputed(() => {
// First check if widget has requested a custom column mapping and // First check if widget has requested a custom column mapping and

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

@ -6,6 +6,7 @@
import {AppModel, reportError} from 'app/client/models/AppModel'; import {AppModel, reportError} from 'app/client/models/AppModel';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo'; 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 {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox'; import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {colors, testId, vars} from 'app/client/ui2018/cssVars'; 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', ` export const cssField = styled('div', `
margin: 16px 0; margin: 16px 0;
display: flex; display: flex;

@ -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;
`);

@ -75,7 +75,7 @@ export function prepareForTransition(elem: HTMLElement, prepare: () => void) {
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565 // 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 // for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow. // 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. // Restore transitions.
elem.style.transitionProperty = prior; elem.style.transitionProperty = prior;

@ -1,6 +1,6 @@
import {FocusLayer} from 'app/client/lib/FocusLayer'; import {FocusLayer} from 'app/client/lib/FocusLayer';
import {reportError} from 'app/client/models/errors'; 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 {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons'; import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars'; import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';

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

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

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

@ -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. // but we got same enough spaces, we will force browser to check the available space once more time.
if (enoughSpace(rect, size) && hasScroll(textInput)) { if (enoughSpace(rect, size) && hasScroll(textInput)) {
textInput.style.overflow = "hidden"; textInput.style.overflow = "hidden";
// eslint-disable-next-line no-unused-expressions
textInput.clientHeight; // just access metrics is enough to repaint textInput.clientHeight; // just access metrics is enough to repaint
textInput.style.overflow = "auto"; textInput.style.overflow = "auto";
} }

@ -14,6 +14,7 @@ import {
dom, dom,
DomContents, DomContents,
fromKo, fromKo,
IDisposableOwnerT,
Observable, Observable,
} from 'grainjs'; } 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. * Override the create() method to match the parameters of create() expected by FieldBuilder.
*/ */
public static create(field: ViewFieldRec) { // We copy Disposable.create() signature (the second one) to pacify typescript, but code must
return Disposable.create.call(this as any, null, field); // 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>; protected options: SaveableObjObservable<any>;

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

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

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

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

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

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

@ -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 CustomSectionAPITI from './CustomSectionAPI-ti';
import FileParserAPITI from './FileParserAPI-ti'; import FileParserAPITI from './FileParserAPI-ti';
import GristAPITI from './GristAPI-ti'; import GristAPITI from './GristAPI-ti';
@ -18,7 +18,14 @@ export {
const allTypes = [ const allTypes = [
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI, 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}>) { function checkDuplicates(types: Array<{[key: string]: object}>) {
const seen = new Set<string>(); const seen = new Set<string>();

@ -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. * 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. * Declare that a component is prepared to receive messages from the outside world.

@ -18,33 +18,6 @@ declare module "bluebird" {
class Disposer<T> {} 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. // Used in one place, and the typings are almost entirely unhelpful.
declare module "multiparty"; declare module "multiparty";

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

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

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

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

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

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

@ -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`. * Returns whether the path `inner` is contained within the directory `outer`.
*/ */

@ -1,10 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "es2016", "target": "es2017",
"module": "commonjs", "module": "commonjs",
"strict": true, "strict": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true, "noImplicitAny": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@ -22,6 +24,7 @@
], ],
}, },
"composite": true, "composite": true,
"types" : [],
"plugins": [{ "plugins": [{
"name": "typescript-eslint-language-service" "name": "typescript-eslint-language-service"
}], }],

@ -3,9 +3,26 @@ const path = require('path');
module.exports = { module.exports = {
target: 'web', target: 'web',
mode: 'production', mode: 'production',
entry: "./_build/app/client/browserCheck.js", entry: "./app/client/browserCheck",
output: { output: {
path: path.resolve("./static"), path: path.resolve("./static"),
filename: "browser-check.js" filename: "browser-check.js"
}, },
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|ts)?$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2017',
sourcemap: true,
},
exclude: /node_modules/
},
]
}
}; };

@ -1,13 +1,13 @@
const StatsPlugin = require('stats-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const { ProvidePlugin } = require('webpack');
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
target: 'web', target: 'web',
entry: { entry: {
main: "app/client/app.js", main: "app/client/app",
errorPages: "app/client/errorMain.js", errorPages: "app/client/errorMain",
account: "app/client/accountMain.js", account: "app/client/accountMain",
}, },
output: { output: {
filename: "[name].bundle.js", filename: "[name].bundle.js",
@ -32,15 +32,29 @@ module.exports = {
// typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken). // typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken).
devtool: "source-map", devtool: "source-map",
resolve: { resolve: {
extensions: ['.ts', '.js'],
modules: [ modules: [
path.resolve('./_build'), path.resolve('.'),
path.resolve('./_build/ext'), path.resolve('./ext'),
path.resolve('./_build/stubs'), path.resolve('./stubs'),
path.resolve('./node_modules') path.resolve('./node_modules')
], ],
fallback: {
'path': require.resolve("path-browserify"),
},
}, },
module: { module: {
rules: [ rules: [
{
test: /\.(js|ts)?$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2017',
sourcemap: true,
},
exclude: /node_modules/
},
{ test: /\.js$/, { test: /\.js$/,
use: ["source-map-loader"], use: ["source-map-loader"],
enforce: "pre" enforce: "pre"
@ -48,10 +62,11 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new StatsPlugin( // Some modules assume presence of Buffer and process.
'../.build_stats_js_bundle', // relative to output folder new ProvidePlugin({
{source: false}, // Omit sources, which unnecessarily make the stats file huge. process: 'process/browser',
), Buffer: ['buffer', 'Buffer']
}),
// To strip all locales except “en” // To strip all locales except “en”
new MomentLocalesPlugin() new MomentLocalesPlugin()
], ],

@ -45,9 +45,8 @@
"@types/mime-types": "2.1.0", "@types/mime-types": "2.1.0",
"@types/mocha": "5.2.5", "@types/mocha": "5.2.5",
"@types/moment-timezone": "0.5.9", "@types/moment-timezone": "0.5.9",
"@types/node": "^10", "@types/node": "^14",
"@types/node-fetch": "2.1.2", "@types/node-fetch": "2.1.2",
"@types/numeral": "0.0.25",
"@types/pidusage": "2.0.1", "@types/pidusage": "2.0.1",
"@types/plotly.js": "1.44.15", "@types/plotly.js": "1.44.15",
"@types/qrcode": "1.4.2", "@types/qrcode": "1.4.2",
@ -64,6 +63,7 @@
"catw": "1.0.1", "catw": "1.0.1",
"chai": "4.2.0", "chai": "4.2.0",
"chai-as-promised": "7.1.1", "chai-as-promised": "7.1.1",
"esbuild-loader": "2.19.0",
"mocha": "5.2.0", "mocha": "5.2.0",
"mocha-webdriver": "0.2.9", "mocha-webdriver": "0.2.9",
"moment-locales-webpack-plugin": "^1.2.0", "moment-locales-webpack-plugin": "^1.2.0",
@ -72,11 +72,10 @@
"selenium-webdriver": "3.6.0", "selenium-webdriver": "3.6.0",
"sinon": "7.1.1", "sinon": "7.1.1",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"stats-webpack-plugin": "^0.7.0",
"tmp-promise": "1.0.5", "tmp-promise": "1.0.5",
"typescript": "3.9.3", "typescript": "4.7.4",
"webpack": "4.41.0", "webpack": "5.73.0",
"webpack-cli": "3.3.2", "webpack-cli": "4.10.0",
"why-is-node-running": "2.0.3" "why-is-node-running": "2.0.3"
}, },
"dependencies": { "dependencies": {
@ -127,7 +126,6 @@
"mousetrap": "1.6.2", "mousetrap": "1.6.2",
"multiparty": "4.2.2", "multiparty": "4.2.2",
"node-fetch": "2.2.0", "node-fetch": "2.2.0",
"numeral": "2.0.6",
"pg": "8.6.0", "pg": "8.6.0",
"plotly.js-basic-dist": "1.51.1", "plotly.js-basic-dist": "1.51.1",
"popper-max-size-modifier": "0.2.0", "popper-max-size-modifier": "0.2.0",

@ -1,144 +0,0 @@
from six.moves import xrange
def _is_array(obj):
return isinstance(obj, list)
def get(obj, path):
"""
Looks up and returns a path in the object. Returns None if the path isn't there.
"""
for part in path:
try:
obj = obj[part]
except(KeyError, IndexError):
return None
return obj
def glob(obj, path, func, extra_arg):
"""
Resolves wildcards in `path`, calling func for all matching paths. Returns the number of
times that func was called.
obj - An object to scan.
path - Path to an item in an object or an array in obj. May contain the special key '*', which
-- for arrays only -- means "for all indices".
func - Will be called as func(subobj, key, fullPath, extraArg).
extra_arg - An arbitrary value to pass along to func, for convenience.
Returns count of matching paths, for which func got called.
"""
return _globHelper(obj, path, path, func, extra_arg)
def _globHelper(obj, path, full_path, func, extra_arg):
for i, part in enumerate(path[:-1]):
if part == "*" and _is_array(obj):
# We got an array wildcard
subpath = path[i + 1:]
count = 0
for subobj in obj:
count += _globHelper(subobj, subpath, full_path, func, extra_arg)
return count
try:
obj = obj[part]
except:
raise Exception("gpath.glob: non-existent object at " +
describe(full_path[:len(full_path) - len(path) + i + 1]))
return func(obj, path[-1], full_path, extra_arg) or 1
def place(obj, path, value):
"""
Sets or deletes an object property in DocObj.
gpath - Path to an Object in obj.
value - Any value. Setting None will remove the selected object key.
"""
return glob(obj, path, _placeHelper, value)
def _placeHelper(subobj, key, full_path, value):
if not isinstance(subobj, dict):
raise Exception("gpath.place: not a plain object at " + describe(dirname(full_path)))
if value is not None:
subobj[key] = value
elif key in subobj:
del subobj[key]
def _checkIsArray(subobj, errPrefix, index, itemPath, isInsert):
"""
This is a helper for checking operations on arrays, and throwing descriptive errors.
"""
if subobj is None:
raise Exception(errPrefix + ": non-existent object at " + describe(dirname(itemPath)))
elif not _is_array(subobj):
raise Exception(errPrefix + ": not an array at " + describe(dirname(itemPath)))
else:
length = len(subobj)
validIndex = (isinstance(index, int) and index >= 0 and index < length)
validInsertIndex = (index is None or index == length)
if not (validIndex or (isInsert and validInsertIndex)):
raise Exception(errPrefix + ": invalid array index: " + describe(itemPath))
def insert(obj, path, value):
"""
Inserts an element into an array in DocObj.
gpath - Path to an item in an array in obj.
The new value will be inserted before the item pointed to by gpath.
The last component of gpath may be null, in which case the value is appended at the end.
value - Any value.
"""
return glob(obj, path, _insertHelper, value)
def _insertHelper(subobj, index, fullPath, value):
_checkIsArray(subobj, "gpath.insert", index, fullPath, True)
if index is None:
subobj.append(value)
else:
subobj.insert(index, value)
def update(obj, path, value):
"""
Updates an element in an array in DocObj.
gpath - Path to an item in an array in obj.
value - Any value.
"""
return glob(obj, path, _updateHelper, value)
def _updateHelper(subobj, index, fullPath, value):
if index == '*':
_checkIsArray(subobj, "gpath.update", None, fullPath, True)
for i in xrange(len(subobj)):
subobj[i] = value
return len(subobj)
else:
_checkIsArray(subobj, "gpath.update", index, fullPath, False)
subobj[index] = value
def remove(obj, path):
"""
Removes an element from an array in DocObj.
gpath - Path to an item in an array in obj.
"""
return glob(obj, path, _removeHelper, None)
def _removeHelper(subobj, index, fullPath, _):
_checkIsArray(subobj, "gpath.remove", index, fullPath, False)
del subobj[index]
def dirname(path):
"""
Returns path without the last component, like a directory name in a filesystem path.
"""
return path[:-1]
def basename(path):
"""
Returns the last component of path, like base name of a filesystem path.
"""
return path[-1] if path else None
def describe(path):
"""
Returns a human-readable representation of path.
"""
return "/" + "/".join(str(p) for p in path)

@ -1,159 +0,0 @@
import unittest
import gpath
class TestGpath(unittest.TestCase):
def setUp(self):
self.obj = {
"foo": [{"bar": 1}, {"bar": 2}, {"baz": 3}],
"hello": "world"
}
def test_get(self):
self.assertEqual(gpath.get(self.obj, ["foo", 0, "bar"]), 1)
self.assertEqual(gpath.get(self.obj, ["foo", 2]), {"baz": 3})
self.assertEqual(gpath.get(self.obj, ["hello"]), "world")
self.assertEqual(gpath.get(self.obj, []), self.obj)
self.assertEqual(gpath.get(self.obj, ["foo", 0, "baz"]), None)
self.assertEqual(gpath.get(self.obj, ["foo", 4]), None)
self.assertEqual(gpath.get(self.obj, ["foo", 4, "baz"]), None)
self.assertEqual(gpath.get(self.obj, [0]), None)
def test_set(self):
gpath.place(self.obj, ["foo"], {"bar": 1, "baz": 2})
self.assertEqual(self.obj["foo"], {"bar": 1, "baz": 2})
gpath.place(self.obj, ["foo", "bar"], 17)
self.assertEqual(self.obj["foo"], {"bar": 17, "baz": 2})
gpath.place(self.obj, ["foo", "baz"], None)
self.assertEqual(self.obj["foo"], {"bar": 17})
self.assertEqual(self.obj["hello"], "world")
gpath.place(self.obj, ["hello"], None)
self.assertFalse("hello" in self.obj)
gpath.place(self.obj, ["hello"], None) # OK to remove a non-existent property.
self.assertFalse("hello" in self.obj)
gpath.place(self.obj, ["hello"], "blah")
self.assertEqual(self.obj["hello"], "blah")
def test_set_strict(self):
with self.assertRaisesRegex(Exception, r"non-existent"):
gpath.place(self.obj, ["bar", 4], 17)
with self.assertRaisesRegex(Exception, r"not a plain object"):
gpath.place(self.obj, ["foo", 0], 17)
def test_insert(self):
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.insert(self.obj, ["foo", 0], "asdf")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.insert(self.obj, ["foo", 3], "hello")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}])
gpath.insert(self.obj, ["foo", None], "world")
self.assertEqual(self.obj["foo"],
["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}, "world"])
def test_insert_strict(self):
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.insert(self.obj, ["foo"], "asdf")
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.insert(self.obj, ["foo", -1], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.insert(self.obj, ["foo", "foo"], 17)
def test_update(self):
"""update should update array items"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.update(self.obj, ["foo", 0], "asdf")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, {"baz": 3}])
gpath.update(self.obj, ["foo", 2], "hello")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, "hello"])
gpath.update(self.obj, ["foo", 1], None)
self.assertEqual(self.obj["foo"], ["asdf", None, "hello"])
def test_update_strict(self):
"""update should be strict"""
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.update(self.obj, ["bar", 4], 17)
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.update(self.obj, ["foo"], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.update(self.obj, ["foo", -1], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.update(self.obj, ["foo", None], 17)
def test_remove(self):
"""remove should remove indices"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.remove(self.obj, ["foo", 0])
self.assertEqual(self.obj["foo"], [{"bar": 2}, {"baz": 3}])
gpath.remove(self.obj, ["foo", 1])
self.assertEqual(self.obj["foo"], [{"bar": 2}])
gpath.remove(self.obj, ["foo", 0])
self.assertEqual(self.obj["foo"], [])
def test_remove_strict(self):
"""remove should be strict"""
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.remove(self.obj, ["bar", 4])
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.remove(self.obj, ["foo"])
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.remove(self.obj, ["foo", -1])
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.remove(self.obj, ["foo", None])
def test_glob(self):
"""glob should scan arrays"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
self.assertEqual(gpath.place(self.obj, ["foo", "*", "bar"], 17), 3)
self.assertEqual(self.obj["foo"], [{"bar": 17}, {"bar": 17}, {"baz": 3, "bar": 17}])
with self.assertRaisesRegex(Exception, r'non-existent object at \/foo\/\*\/bad'):
gpath.place(self.obj, ["foo", "*", "bad", "test"], 10)
self.assertEqual(gpath.update(self.obj, ["foo", "*"], "hello"), 3)
self.assertEqual(self.obj["foo"], ["hello", "hello", "hello"])
def test_glob_strict_wildcard(self):
"""should only support tail wildcard for updates"""
with self.assertRaisesRegex(Exception, r'invalid array index'):
gpath.remove(self.obj, ["foo", "*"])
with self.assertRaisesRegex(Exception, r'invalid array index'):
gpath.insert(self.obj, ["foo", "*"], 1)
def test_glob_wildcard_keys(self):
"""should not scan object keys"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
self.assertEqual(gpath.place(self.obj, ["foo", 0, "*"], 17), 1)
self.assertEqual(self.obj["foo"], [{"bar": 1, '*': 17}, {"bar": 2}, {"baz": 3}])
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.place(self.obj, ["*", 0, "bar"], 17)
def test_glob_nested(self):
"""should scan nested arrays"""
self.obj = [{"a": [1,2,3]}, {"a": [4,5,6]}, {"a": [7,8,9]}]
self.assertEqual(gpath.update(self.obj, ["*", "a", "*"], 5), 9)
self.assertEqual(self.obj, [{"a": [5,5,5]}, {"a": [5,5,5]}, {"a": [5,5,5]}])
def test_dirname(self):
"""dirname should return path without last component"""
self.assertEqual(gpath.dirname(["foo", "bar", "baz"]), ["foo", "bar"])
self.assertEqual(gpath.dirname([1, 2]), [1])
self.assertEqual(gpath.dirname(["foo"]), [])
self.assertEqual(gpath.dirname([]), [])
def test_basename(self):
"""basename should return the last component of path"""
self.assertEqual(gpath.basename(["foo", "bar", "baz"]), "baz")
self.assertEqual(gpath.basename([1, 2]), 2)
self.assertEqual(gpath.basename(["foo"]), "foo")
self.assertEqual(gpath.basename([]), None)
if __name__ == "__main__":
unittest.main()

@ -16,7 +16,7 @@ import {Client, ClientMethod} from 'app/server/lib/Client';
import {CommClientConnect} from 'app/common/CommTypes'; import {CommClientConnect} from 'app/common/CommTypes';
import {delay} from 'app/common/delay'; import {delay} from 'app/common/delay';
import {isLongerThan} from 'app/common/gutil'; import {isLongerThan} from 'app/common/gutil';
import {fromCallback, getAvailablePort} from 'app/server/lib/serverUtils'; import {connect as connectSock, fromCallback, getAvailablePort, listenPromise} from 'app/server/lib/serverUtils';
import {Sessions} from 'app/server/lib/Sessions'; import {Sessions} from 'app/server/lib/Sessions';
import * as testUtils from 'test/server/testUtils'; import * as testUtils from 'test/server/testUtils';
import * as session from '@gristlabs/express-session'; import * as session from '@gristlabs/express-session';
@ -52,7 +52,7 @@ describe('Comm', function() {
server = http.createServer(); server = http.createServer();
comm = new Comm(server, {sessions}); comm = new Comm(server, {sessions});
comm.registerMethods(methods); comm.registerMethods(methods);
return fromCallback(cb => server.listen(0, 'localhost', cb)); return listenPromise(server.listen(0, 'localhost'));
} }
async function stopComm() { async function stopComm() {
@ -500,8 +500,7 @@ export class TcpForwarder {
public async connect() { public async connect() {
await this.disconnect(); await this.disconnect();
this._server = new Server((sock) => this._onConnect(sock)); this._server = new Server((sock) => this._onConnect(sock));
await new Promise((resolve, reject) => await listenPromise(this._server.listen(this.port));
this._server!.on('error', reject).listen(this.port, resolve));
} }
public async disconnectClientSide() { public async disconnectClientSide() {
await Promise.all(Array.from(this._connections.keys(), destroySock)); await Promise.all(Array.from(this._connections.keys(), destroySock));
@ -528,9 +527,7 @@ export class TcpForwarder {
} }
} }
private async _onConnect(clientSock: Socket) { private async _onConnect(clientSock: Socket) {
const serverSock = new Socket(); const serverSock = await connectSock(this._serverPort);
await new Promise((resolve, reject) =>
serverSock.on('error', reject).connect(this._serverPort, resolve));
clientSock.pipe(serverSock); clientSock.pipe(serverSock);
serverSock.pipe(clientSock); serverSock.pipe(clientSock);
clientSock.on('error', (err) => serverSock.destroy(err)); clientSock.on('error', (err) => serverSock.destroy(err));

@ -1,5 +1,5 @@
import {getAppRoot} from 'app/server/lib/places'; import {getAppRoot} from 'app/server/lib/places';
import {fromCallback} from 'app/server/lib/serverUtils'; import {fromCallback, listenPromise} from 'app/server/lib/serverUtils';
import * as express from 'express'; import * as express from 'express';
import * as http from 'http'; import * as http from 'http';
import {AddressInfo, Socket} from 'net'; import {AddressInfo, Socket} from 'net';
@ -45,7 +45,7 @@ export function serveCustomViews(): Promise<Serving> {
export async function serveSomething(setup: (app: express.Express) => void, port= 0): Promise<Serving> { export async function serveSomething(setup: (app: express.Express) => void, port= 0): Promise<Serving> {
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
await fromCallback(cb => server.listen(port, cb)); await listenPromise(server.listen(port));
const connections = new Set<Socket>(); const connections = new Set<Socket>();
server.on('connection', (conn) => { server.on('connection', (conn) => {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save