(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>;