projectstorm / react-diagrams

a super simple, no-nonsense diagramming library written in react that just works
https://projectstorm.cloud/react-diagrams
MIT License
8.58k stars 1.17k forks source link

Proposal: remove lodash (?) #787

Open alexandernst opened 3 years ago

alexandernst commented 3 years ago

I'm currently exploring ways in which this package's size can be reduced. It seems like lodash is quite a big dependency (715kb uncompressed / not gzipped).

Digging in the source code, these are all the places where lodash is used:

grep -rnI "_\." --exclude-dir=node_modules --include \*.ts --include \*.tsx *                                                                                                                                           
packages/react-canvas-core/src/core/FactoryBank.ts:32:      return _.values(this.factories);
packages/react-canvas-core/src/core-state/State.ts:76:      return _.intersection(this.keys, keys).length === this.keys.length;
packages/react-canvas-core/src/core-state/StateMachine.ts:32:       this.setState(_.last(this.stateStack));
packages/react-canvas-core/src/actions/DeleteItemsAction.ts:34:             if (keyCodes.indexOf(keyCode) !== -1 && _.isEqual({ ctrlKey, shiftKey, altKey, metaKey }, modifiers)) {
packages/react-canvas-core/src/actions/DeleteItemsAction.ts:35:                 _.forEach(this.engine.getModel().getSelectedEntities(), (model) => {
packages/react-canvas-core/src/core-actions/ActionEventBus.ts:19:       return _.keys(this.keys);
packages/react-canvas-core/src/core-actions/ActionEventBus.ts:36:       return _.filter(this.actions, (action) => {
packages/react-canvas-core/src/core-models/BaseEntity.ts:63:        let clone = _.cloneDeep(this);
packages/react-canvas-core/src/widgets/PeformanceWidget.tsx:28:     return !_.isEqual(this.props.serialized, nextProps.serialized);
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:50:       return _.flatMap(this.layers, (layer) => {
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:56:       return _.filter(this.getSelectionEntities(), (ob) => {
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:62:       _.forEach(this.getSelectedEntities(), (element) => {
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:68:       return _.flatMap(this.layers, (layer) => {
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:69:           return _.values(layer.getModels());
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:147:      _.forEach(event.data.layers, (layer) => {
packages/react-canvas-core/src/entities/canvas/CanvasModel.ts:166:          layers: _.map(this.layers, (layer) => {
packages/react-canvas-core/src/entities/layer/LayerModel.ts:40:     _.forEach(event.data.models, (model) => {
packages/react-canvas-core/src/entities/layer/LayerModel.ts:57:         models: _.mapValues(this.models, (model) => {
packages/react-canvas-core/src/entities/layer/LayerModel.ts:84:     return _.flatMap(this.models, (model) => {
packages/react-diagrams-defaults/src/node/DefaultNodeModel.ts:97:       this.portsIn = _.map(event.data.portsInOrder, (id) => {
packages/react-diagrams-defaults/src/node/DefaultNodeModel.ts:100:      this.portsOut = _.map(event.data.portsOutOrder, (id) => {
packages/react-diagrams-defaults/src/node/DefaultNodeModel.ts:110:          portsInOrder: _.map(this.portsIn, (port) => {
packages/react-diagrams-defaults/src/node/DefaultNodeModel.ts:113:          portsOutOrder: _.map(this.portsOut, (port) => {
packages/react-diagrams-defaults/src/node/DefaultNodeWidget.tsx:77:                 <S.PortsContainer>{_.map(this.props.node.getInPorts(), this.generatePort)}</S.PortsContainer>
packages/react-diagrams-defaults/src/node/DefaultNodeWidget.tsx:78:                 <S.PortsContainer>{_.map(this.props.node.getOutPorts(), this.generatePort)}</S.PortsContainer>
packages/react-diagrams-core/src/models/DiagramModel.ts:52:     return _.filter(this.layers, (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:58:     return _.filter(this.layers, (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:106:        _.forEach(models, (model) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:135:        const removed = _.some(this.getLinkLayers(), (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:144:        const removed = _.some(this.getNodeLayers(), (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:153:        return _.flatMap(this.getLinkLayers(), (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:154:            return _.values(layer.getModels());
packages/react-diagrams-core/src/models/DiagramModel.ts:159:        return _.flatMap(this.getNodeLayers(), (layer) => {
packages/react-diagrams-core/src/models/DiagramModel.ts:160:            return _.values(layer.getModels());
packages/react-diagrams-core/src/states/DragDiagramItemsState.ts:18:                        _.forEach(this.initialPositions, (position) => {
packages/react-diagrams-core/src/entities/node-layer/NodeLayerWidget.tsx:22:                {_.map(this.props.layer.getNodes(), (node: NodeModel) => {
packages/react-diagrams-core/src/entities/link-layer/LinkLayerWidget.tsx:23:                    _.map(this.props.layer.getLinks(), (link) => {
packages/react-diagrams-core/src/entities/link/LinkWidget.tsx:110:                          {_.map(this.props.link.getLabels(), (labelModel, index) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:56:         _.map(this.points, (point: PointModel) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:64:         return super.getSelectionEntities().concat(_.slice(this.points, 1, this.points.length - 1));
packages/react-diagrams-core/src/entities/link/LinkModel.ts:68:         return super.getSelectionEntities().concat(_.slice(this.points, 0, this.points.length - 1));
packages/react-diagrams-core/src/entities/link/LinkModel.ts:72:         return super.getSelectionEntities().concat(_.slice(this.points, 1, this.points.length));
packages/react-diagrams-core/src/entities/link/LinkModel.ts:79:     this.points = _.map(event.data.points || [], (point) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:92:     _.forEach(event.data.labels || [], (label: any) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:130:            points: _.map(this.points, (point) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:133:            labels: _.map(this.labels, (label) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:141:            _.map(this.getPoints(), (point: PointModel) => {
packages/react-diagrams-core/src/entities/link/LinkModel.ts:265:        _.forEach(points, (point) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:49:     _.forEach(this.ports, (port) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:58:     _.forEach(event.data.ports, (port: any) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:73:         ports: _.map(this.ports, (port) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:82:     _.forEach(this.ports, (port) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:89:     _.forEach(this.ports, (port) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:90:         _.forEach(port.getLinks(), (link) => {
packages/react-diagrams-core/src/entities/node/NodeModel.ts:124:        for (let link of _.values(port.getLinks())) {
packages/react-diagrams-core/src/entities/node/NodeWidget.tsx:66:           _.forEach(this.props.node.getPorts(), (port) => {
packages/react-diagrams-core/src/entities/port/PortModel.ts:67:         links: _.map(this.links, (link) => {
packages/react-diagrams-core/src/entities/port/PortModel.ts:78:     _.forEach(this.getLinks(), (link) => {
packages/react-diagrams-core/src/entities/port/PortModel.ts:118:        if (_.isFinite(this.options.maximumLinks)) {
packages/react-diagrams-core/src/entities/port/PortModel.ts:119:            var numberOfLinks: number = _.size(this.links);
packages/react-diagrams-core/src/entities/port/PortModel.ts:121:                return _.values(this.links)[0];
packages/react-diagrams-core/src/entities/port/PortModel.ts:130:        _.forEach(this.getLinks(), (link) => {
packages/react-diagrams-core/src/entities/port/PortWidget.tsx:50:           const links = _.keys(this.props.port.getNode().getPort(this.props.port.getName()).links).join(',');
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:23:     return _.map(attribute.split(','), (id) => {
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:31:     let currentLinks = _.map(await this.getLinks(), 'name');
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:45:     let newLinks = _.map(await this.getLinks(), 'name');
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:47:     const s = new E2ELink(_.difference(newLinks, currentLinks)[0]);
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:53:     let currentLinks = _.map(await this.getLinks(), 'id');
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:65:     let newLinks = _.map(await this.getLinks(), 'id');
packages/diagrams-demo-gallery/tests-e2e/helpers/E2EPort.ts:67:     const link = _.difference(newLinks, currentLinks)[0];
packages/diagrams-demo-gallery/demos/demo-mutate-graph/index.tsx:33:        let node: ReturnType<NodeModel['serialize']> = _.values(obj.layers[1].models)[0] as any;
packages/diagrams-demo-gallery/demos/demo-drag-and-drop/components/BodyWidget.tsx:60:                           var nodesCount = _.keys(this.props.app.getDiagramEngine().getModel().getNodes()).length;
packages/diagrams-demo-gallery/demos/demo-custom-action/index.tsx:29:                           _.forEach(selectedEntities, (model) => {
packages/diagrams-demo-gallery/demos/demo-dynamic-ports/index.tsx:10:       const nodes: DefaultNodeModel[] = _.values(this.props.model.getNodes()) as DefaultNodeModel[];
packages/diagrams-demo-gallery/demos/demo-cloning/index.tsx:23:     _.forEach(model.getSelectedEntities(), (item: BaseModel<any>) => {
packages/react-diagrams-routing/src/link/PathFindingLinkWidget.tsx:82:      const directPathCoords = this.pathFinding.calculateDirectPath(_.first(points), _.last(points));
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:57:             _.defer(() => {
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:115:        this.canvasMatrix = _.range(0, matrixHeight).map(() => {
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:142:        const matrix = _.cloneDeep(this.getCanvasMatrix());
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:174:        const allNodesCoords = _.values(this.engine.getModel().getNodes()).map((item) => ({
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:181:        const allLinks = _.values(this.engine.getModel().getLinks());
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:182:        const allPortsCoords = _.flatMap(allLinks.map((link) => [link.getSourcePort(), link.getTargetPort()]))
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:190:        const allPointsCoords = _.flatMap(allLinks.map((link) => link.getPoints())).map((item) => ({
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:198:        const sumProps = (object, props) => _.reduce(props, (acc, prop) => acc + _.get(object, prop, 0), 0);
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:201:        const concatedCoords = _.concat(allNodesCoords, allPortsCoords, allPointsCoords);
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:203:            Math.floor(Math.min(_.get(_.minBy(concatedCoords, 'x'), 'x', 0), 0) / this.ROUTING_SCALING_FACTOR) *
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:205:        const maxXElement = _.maxBy(concatedCoords, (item) => sumProps(item, ['x', 'width']));
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:207:        const minYCoords = _.minBy(concatedCoords, 'y');
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:209:            Math.floor(Math.min(_.get(minYCoords, 'y', 0), 0) / this.ROUTING_SCALING_FACTOR) * this.ROUTING_SCALING_FACTOR;
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:210:        const maxYElement = _.maxBy(concatedCoords, (item) => sumProps(item, ['y', 'height']));
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:225:        _.values(this.engine.getModel().getNodes()).forEach((node) => {
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:243:        const allElements = _.flatMap(
packages/react-diagrams-routing/src/link/PathFindingLinkFactory.tsx:244:            _.values(this.engine.getModel().getLinks()).map((link) => [].concat(link.getSourcePort(), link.getTargetPort()))
packages/react-diagrams-routing/src/dagre/DagreEngine.ts:35:        _.forEach(model.getNodes(), (node) => {
packages/react-diagrams-routing/src/dagre/DagreEngine.ts:39:        _.forEach(model.getLinks(), (link) => {
packages/geometry/src/Polygon.ts:13:        return _.map(this.points, (point) => {
packages/geometry/src/Polygon.ts:19:        this.points = _.map(data, (point) => {
packages/geometry/src/Polygon.ts:26:        _.forEach(this.points, (point) => {
packages/geometry/src/Polygon.ts:32:        _.forEach(this.points, (point) => {
packages/geometry/src/Polygon.ts:50:        _.forEach(this.points, (point) => {
packages/geometry/src/Polygon.ts:56:        this.points = _.map(ob.points, (point) => {
packages/geometry/src/Polygon.ts:77:            _.flatMap(polygons, (polygon) => {

A list of each method and the times it's being used:

grep -h -rI "_\." --exclude-dir=node_modules --include \*.ts --include \*.tsx * | grep -Eio '_\.[a-zA-Z]+' | sort | uniq -c | sort -nr                                                                                     
  25 _.map
  23 _.forEach
  12 _.values
   9 _.flatMap
   4 _.filter
   3 _.slice
   3 _.keys
   3 _.get
   2 _.some
   2 _.minBy
   2 _.maxBy
   2 _.last
   2 _.isEqual
   2 _.difference
   2 _.cloneDeep
   1 _.size
   1 _.reduce
   1 _.range
   1 _.mapValues
   1 _.isFinite
   1 _.intersection
   1 _.first
   1 _.defer
   1 _.concat

The following methods:

map, forEach, values, filter, slice, keys, some, reduce and concat

are already implemented in the various versions of ES, which means that it's safe to use the native implementations. Should there be a requirement to support older browsers, babel should take care of the polyfills automatically.

Which leaves us with flatMap, get, minBy, maxBy, last, isEqual, difference, cloneDeep, size, range, mapValues, isFinite, intersection, first and defer.

Are you open to the possibility of having implemented these functions inside react-diagrams and drop the lodash package? This will greatly reduce the size of the final bundle.

alexandernst commented 3 years ago

WIP here: https://github.com/develatio/react-diagrams