From da5db714c2ba49f789bea68971a2b46c29ff6b0a Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Thu, 6 May 2021 00:28:22 +0100 Subject: [PATCH] Remove `lodash` from core (#2827) * Remove `lodash-es` dependency * Replace `escapeRegExp` with home-made util * Replace `throttle` with `throttle-debounce` library * Use native browser methods for `deepFlatten` We need a polyfill for iOS 11 and below. I think using a native method with this polyfill is better than having our own function instead, even if the bundle size is ~150B more. * Save a few bytes in `escapeRegExp` * Fix typo in comment * Undo import re-organisation * Use spread instead of slice * Use smaller Array.flat polyfill from MDN * Export new utils in `compat.js` --- js/package-lock.json | 58 +++++-------------- js/package.json | 5 +- js/src/common/Application.js | 3 +- js/src/common/compat.js | 4 ++ js/src/common/index.js | 2 + js/src/common/utils/arrayFlatPolyfill.ts | 14 +++++ js/src/common/utils/escapeRegExp.ts | 10 ++++ js/src/common/utils/throttleDebounce.ts | 3 + js/src/forum/components/DiscussionListItem.js | 2 +- js/src/forum/states/PostStreamState.js | 6 +- 10 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 js/src/common/utils/arrayFlatPolyfill.ts create mode 100644 js/src/common/utils/escapeRegExp.ts create mode 100644 js/src/common/utils/throttleDebounce.ts diff --git a/js/package-lock.json b/js/package-lock.json index 40fc09b52..cee21a1fb 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -15,16 +15,15 @@ "expose-loader": "^1.0.3", "jquery": "^3.6.0", "jquery.hotkeys": "^0.1.0", - "lodash-es": "^4.17.21", "mithril": "^2.0.4", "punycode": "^2.1.1", "spin.js": "^3.1.0", - "textarea-caret": "^3.1.0" + "textarea-caret": "^3.1.0", + "throttle-debounce": "^3.0.1" }, "devDependencies": { "@babel/preset-typescript": "^7.13.0", "@types/jquery": "^3.5.5", - "@types/lodash-es": "^4.17.4", "@types/mithril": "^2.0.7", "@types/punycode": "^2.1.0", "@types/textarea-caret": "^3.0.0", @@ -1498,21 +1497,6 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, - "node_modules/@types/lodash": { - "version": "4.14.168", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", - "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", - "dev": true - }, - "node_modules/@types/lodash-es": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.4.tgz", - "integrity": "sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/mithril": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.0.7.tgz", @@ -4669,11 +4653,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6660,6 +6639,14 @@ "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -8885,21 +8872,6 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, - "@types/lodash": { - "version": "4.14.168", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", - "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", - "dev": true - }, - "@types/lodash-es": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.4.tgz", - "integrity": "sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, "@types/mithril": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.0.7.tgz", @@ -11399,11 +11371,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -13016,6 +12983,11 @@ "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" }, + "throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", diff --git a/js/package.json b/js/package.json index 71d626ed5..09b8b9f27 100644 --- a/js/package.json +++ b/js/package.json @@ -11,16 +11,15 @@ "expose-loader": "^1.0.3", "jquery": "^3.6.0", "jquery.hotkeys": "^0.1.0", - "lodash-es": "^4.17.21", "mithril": "^2.0.4", "punycode": "^2.1.1", "spin.js": "^3.1.0", - "textarea-caret": "^3.1.0" + "textarea-caret": "^3.1.0", + "throttle-debounce": "^3.0.1" }, "devDependencies": { "@babel/preset-typescript": "^7.13.0", "@types/jquery": "^3.5.5", - "@types/lodash-es": "^4.17.4", "@types/mithril": "^2.0.7", "@types/punycode": "^2.1.0", "@types/textarea-caret": "^3.0.0", diff --git a/js/src/common/Application.js b/js/src/common/Application.js index 57193cbce..3f160bcbd 100644 --- a/js/src/common/Application.js +++ b/js/src/common/Application.js @@ -20,7 +20,6 @@ import Discussion from './models/Discussion'; import Post from './models/Post'; import Group from './models/Group'; import Notification from './models/Notification'; -import { flattenDeep } from 'lodash-es'; import PageState from './states/PageState'; import ModalManagerState from './states/ModalManagerState'; import AlertManagerState from './states/AlertManagerState'; @@ -184,7 +183,7 @@ export default class Application { Object.keys(extensions).forEach((name) => { const extension = extensions[name]; - const extenders = flattenDeep(extension.extend); + const extenders = extension.extend.flat(Infinity); for (const extender of extenders) { extender.extend(this, { name, exports: extension }); diff --git a/js/src/common/compat.js b/js/src/common/compat.js index 6c4d5898b..c640038ca 100644 --- a/js/src/common/compat.js +++ b/js/src/common/compat.js @@ -12,7 +12,9 @@ import Drawer from './utils/Drawer'; import anchorScroll from './utils/anchorScroll'; import RequestError from './utils/RequestError'; import abbreviateNumber from './utils/abbreviateNumber'; +import escapeRegExp from './utils/escapeRegExp'; import * as string from './utils/string'; +import * as ThrottleDebounce from './utils/throttleDebounce'; import Stream from './utils/Stream'; import SubtreeRetainer from './utils/SubtreeRetainer'; import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh'; @@ -91,6 +93,7 @@ export default { 'utils/abbreviateNumber': abbreviateNumber, 'utils/string': string, 'utils/SubtreeRetainer': SubtreeRetainer, + 'utils/escapeRegExp': escapeRegExp, 'utils/extract': extract, 'utils/ScrollListener': ScrollListener, 'utils/stringToColor': stringToColor, @@ -104,6 +107,7 @@ export default { 'utils/formatNumber': formatNumber, 'utils/mapRoutes': mapRoutes, 'utils/withAttr': withAttr, + 'utils/throttleDebounce': ThrottleDebounce, 'models/Notification': Notification, 'models/User': User, 'models/Post': Post, diff --git a/js/src/common/index.js b/js/src/common/index.js index fe40108f0..c2d576780 100644 --- a/js/src/common/index.js +++ b/js/src/common/index.js @@ -23,3 +23,5 @@ patchMithril(window); import * as Extend from './extend/index'; export { Extend }; + +import './utils/arrayFlatPolyfill'; diff --git a/js/src/common/utils/arrayFlatPolyfill.ts b/js/src/common/utils/arrayFlatPolyfill.ts new file mode 100644 index 000000000..b1ca99011 --- /dev/null +++ b/js/src/common/utils/arrayFlatPolyfill.ts @@ -0,0 +1,14 @@ +// Based off of the polyfill on MDN +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#reduce_concat_isarray_recursivity +// +// Needed to provide support for Safari on iOS < 12 + +if (!Array.prototype['flat']) { + Array.prototype['flat'] = function flat(this: any[], depth: number = 1): any[] { + return depth > 0 + ? Array.prototype.reduce.call(this, (acc, val): any[] => acc.concat(Array.isArray(val) ? flat.call(val, depth - 1) : val), []) + : // If no depth is provided, or depth is 0, just return a copy of + // the array. Spread is supported in all major browsers (iOS 8+) + [...this]; + }; +} diff --git a/js/src/common/utils/escapeRegExp.ts b/js/src/common/utils/escapeRegExp.ts new file mode 100644 index 000000000..f23246b10 --- /dev/null +++ b/js/src/common/utils/escapeRegExp.ts @@ -0,0 +1,10 @@ +const specialChars = /[.*+?^${}()|[\]\\]/g; + +/** + * Escapes the `RegExp` special characters in `input`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +export default function escapeRegExp(input: string): string { + return input.replace(specialChars, '\\$&'); +} diff --git a/js/src/common/utils/throttleDebounce.ts b/js/src/common/utils/throttleDebounce.ts new file mode 100644 index 000000000..3d8758780 --- /dev/null +++ b/js/src/common/utils/throttleDebounce.ts @@ -0,0 +1,3 @@ +// Re-exports `throttle-debounce` to be used in `compat.js`. + +export { throttle, debounce } from 'throttle-debounce'; diff --git a/js/src/forum/components/DiscussionListItem.js b/js/src/forum/components/DiscussionListItem.js index 1fc0adfb3..ed1fa3d5c 100644 --- a/js/src/forum/components/DiscussionListItem.js +++ b/js/src/forum/components/DiscussionListItem.js @@ -15,8 +15,8 @@ import slidable from '../utils/slidable'; import extractText from '../../common/utils/extractText'; import classList from '../../common/utils/classList'; import DiscussionPage from './DiscussionPage'; +import escapeRegExp from '../../common/utils/escapeRegExp'; -import { escapeRegExp } from 'lodash-es'; /** * The `DiscussionListItem` component shows a single discussion in the * discussion list. diff --git a/js/src/forum/states/PostStreamState.js b/js/src/forum/states/PostStreamState.js index b93ec9143..a7cf4166a 100644 --- a/js/src/forum/states/PostStreamState.js +++ b/js/src/forum/states/PostStreamState.js @@ -1,4 +1,4 @@ -import { throttle } from 'lodash-es'; +import { throttle } from 'throttle-debounce'; import anchorScroll from '../../common/utils/anchorScroll'; class PostStreamState { @@ -50,8 +50,8 @@ class PostStreamState { */ this.forceUpdateScrubber = false; - this.loadNext = throttle(this._loadNext, 300); - this.loadPrevious = throttle(this._loadPrevious, 300); + this.loadNext = throttle(300, this._loadNext); + this.loadPrevious = throttle(300, this._loadPrevious); this.show(includedPosts); }