fix: separate the Database and Picker modules

This commit is contained in:
Nolan Lawson 2020-06-01 21:24:46 -07:00
parent 0c60360a6b
commit 8c7305a2e1
14 changed files with 300 additions and 44 deletions

187
README.md
View File

@ -1,43 +1,186 @@
lite-emoji-picker
====
A small, performant, accessible emoji picker and lookup library, distributed as a web component.
A small, performant, accessible emoji picker, distributed as a web component.
Built on top of IndexedDB so it consumes far less memory then other emoji pickers. Suitable for mobile web apps or other resource-constrained environments.
It's built on top of IndexedDB, so it consumes far less memory than other emoji pickers. It also uses [Svelte](https://svelte.dev), so it has a minimal runtime footprint.
Design goals:
- Store emoji in IndexedDB, not memory
- Use IndexedDB for querying
- Render native emoji
- Store emoji data in IndexedDB, not memory
- Render native emoji, no spritesheets
- Accessible by default
- Drop-in as a vanilla web component
## Install
npm install lite-emoji-picker
```js
import { Picker } from 'lite-emoji-picker'
customElements.define('emoji-picker', Picker)
document.body.appendChild(new Picker())
## Usage
```html
<lite-emoji-picker></lite-emoji-picker>
```
```js
import 'lite-emoji-picker';
```
`lite-emoji-picker` will expand to fit whatever container you give it. Here is a good default:
```css
.lite-emoji-picker {
width: 400px;
height: 300px;
}
```
`lite-emoji-picker` uses Shadow DOM, so its inner styling is not accessible except via the API.
## API
### Picker
```js
import { Picker } from 'lite-emoji-picker';
const picker = new Picker();
document.body.appendChild(picker);
```
`new Picker(options)` supports a few different options:
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| `locale` | String | `"en"` | Locale, should map to the locales supported by `emojibase-data` |
| `dataSource` | String | `"https://cdn.jsdelivr.net/npm/emojibase-data@5/en/data.json"` | Where to fetch the emoji data from. Note that `lite-emoji-picker` requires the full `data.json`, not `compact.json`. |
| `i18n` | Object | See below | Strings to use for i18n in the Picker itself, i.e. the text and `aria-label`s.
| `locale` | String | `"en"` | Locale, should map to the locales supported by `emojibase-data` |
| `i18n` | Object | See below | Strings to use for internationalization of the Picker itself, i.e. the text and `aria-label`s. Note that `lite-emoji-picker` only ships with English by default. |
| `numColumns` | number | `8` | Number of emoji to show per row. |
| `darkMode` | boolean/String | `"auto"` | Dark mode. Either `false`, `true`, or `"auto"`. `"auto"` chooses based on [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
| `darkMode` | boolean/String | `"auto"` | Dark mode. Either `false`, `true`, or `"auto"`. The `"auto"` option chooses based on [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
These values can also be set at runtime, e.g.:
```js
const picker = new Picker();
picker.numColumns = 6;
```
#### i18n structure
Note that some of these values are only visible to users of screen readers (but you should still support them if you internationalize your app!).
```json
{
"loading": "Loading…",
"regionLabel": "Emoji picker",
"search": "Search",
"categoriesLabel": "Categories",
"categories": {
"smileys-emotion": "Smileys and emoticons",
"people-body": "People and body",
"animals-nature": "Animals and nature",
"food-drink": "Food and drink",
"travel-places": "Travel and places",
"activities": "Activities",
"objects": "Objects",
"symbols": "Symbols",
"flags": "Flags"
}
}
```
### Database
You can work with the database API separately:
```js
import { Database } from 'lite-emoji-picker';
const database = new Database();
await database.getEmojiBySearchPrefix('elephant'); // [{unicode: "🐘", ...}]
```
`new Database()` takes similar options as the picker:
```js
const database = new Database({
locale: 'en',
dataSource: 'https://cdn.jsdelivr.net/npm/emojibase-data@5/en/data.json'
});
```
In general, it's not a problem to create multiple `Database` objects with the same arguments. Under the hood, they will share the same IndexedDB connection.
<!-- TODO: Database API here -->
### Tree-shaking
If you want to import the `Database` without the `Picker`, or you want to import them separately, then do:
```js
import Picker from 'lite-emoji-picker/dist/es/Picker.js';
import Database from 'lite-emoji-picker/dist/es/Database.js';
```
The reason for this is that `Picker` automatically registers itself as a custom element, following [web component best practices](https://justinfagnani.com/2019/11/01/how-to-publish-web-components-to-npm/). But this adds side effects, so bundlers do not tree-shake as well unless the modules are imported from completely separate files.
## Focus outline
By default, `lite-emoji-picker` displays a prominent focus ring for accessibility reasons. If you want to hide the focus ring for non-keyboard users (e.g. mouse and touch only), then use the [focus-visible](https://github.com/WICG/focus-visible) polyfill, e.g.:
```css
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
}
```
```js
import { applyFocusVisiblePolyfill } from 'focus-visible';
const picker = new Picker();
applyFocusVisiblePolyfill(picker.shadowRoot);
```
`lite-emoji-picker` already ships with the proper CSS for both the `:focus_visible` standard and the polyfill.
## Data and offline
### Data source and JSON format
You can fetch the emoji JSON file from wherever you want. However, it's recommended that your server expose an `ETag` header. If so, `lite-emoji-picker` can avoid re-downloading the entire JSON file over and over again. Instead, it will fire off a `HEAD` request and just check the `ETag`.
If your server is cross-origin, then it will also need to expose `Access-Control-Allow-Origin: *` and `Access-Control-Allow-Headers: *`. (Note that `jsdelivr` already does this, which is why it is the default.)
`lite-emoji-picker` requires the _full_ [emojibase-data](https://github.com/milesj/emojibase) JSON file, not the "compact" one. If you would like to trim the JSON file down even further, then modify the file to only contain these keys:
```json
[
"annotation", "emoji", "emoticon", "group",
"order", "shortcodes", "tags", "version"
]
```
### Offline-first
By default, `lite-emoji-picker` will use the "stale while revalidate" strategy the second time it loads. In other words, it will use any existing data it finds in IndexedDB, and lazily update via the `dataSource` in case data has changed. This means it will work offline-first the second time it runs.
If you would like to manage the database yourself (e.g. to ensure that it's correctly populated before displaying the `Picker`), then create a new `Database` instance and wait for its `ready()` promise to resolve:
```js
const database = new Database()
try {
await database.ready()
} catch (err) {
// Deal with any errors (e.g. offline)
}
```
If `lite-emoji-picker` fails to fetch the JSON data the first time it loads, then it will display no emoji.
## Design decisions
### IndexedDB
The [`emojibase-data`](https://github.com/milesj/emojibase) English JSON file is [854kB](https://unpkg.com/browse/emojibase-data@5.0.1/en/). The "compact" version is still 543kB. That's a lot of data to keep in memory just for an emoji picker. And it's not like that number is ever going down; the Unicode Consortium keeps adding more emoji every year.
The [`emojibase-data`](https://github.com/milesj/emojibase) English JSON file is [854kB](https://unpkg.com/browse/emojibase-data@5.0.1/en/), and the "compact" version is still 543kB. That's a lot of data to keep in memory just for an emoji picker. And it's not like that number is ever going down; the Unicode Consortium keeps adding more emoji every year.
Using IndexedDB has a few advantages:
@ -46,6 +189,20 @@ Using IndexedDB has a few advantages:
### Native emoji
To avoid downloading a large sprite sheet (or deal with potential IP issues), `lite-emoji-picker` only renders native emoji. This means it is limited to the emoji actually installed on the user's device.
To avoid downloading a large sprite sheet (which may be copyrighted, or may look out-of-place on different platforms), `lite-emoji-picker` only renders native emoji. This means it is limited to the emoji actually installed on the user's device.
To avoid rendering ugly unsupported or half-supported emoji, `lite-emoji-picker` will detect emoji support and only render the supported characters. (So no empty boxes or double-width characters.)
To avoid rendering ugly unsupported or half-supported emoji, `lite-emoji-picker` will automatically detect emoji support and only render the supported characters. (So no empty boxes or awkward double emoji.)
### Browser support
Only the latest versions of Chrome, Firefox, and Safari (and browsers using equivalent rendering engines) are supported.
## Contributing
Install
yarn
Run the tests:
yarn test

View File

@ -7,10 +7,14 @@
"files": [
"dist"
],
"sideEffects": false,
"scripts": {
"build": "NODE_ENV=production rollup -c",
"dev": "NODE_ENV=development rollup -c -w",
"build:clean": "rm -fr dist && mkdir -p dist/cjs dist/es",
"build:copy": "cp src/index.cjs.js dist/cjs/index.js && cp src/index.es.js dist/es/index.js",
"build:static": "run-s build:clean build:copy",
"build:rollup": "NODE_ENV=production rollup -c",
"dev:rollup": "NODE_ENV=development rollup -c -w",
"build": "run-s build:static build:rollup",
"dev": "run-s build:static dev:rollup",
"lint": "standard",
"lint:fix": "standard --fix",
"test": "echo \"Error: no test specified\" && exit 1"
@ -43,6 +47,7 @@
"if-emoji": "^0.1.0",
"lint-staged": "^10.2.7",
"lodash-es": "^4.17.15",
"npm-run-all": "^4.1.5",
"rollup": "^2.8.2",
"rollup-plugin-svelte": "^5.2.1",
"rollup-plugin-svelte-hot": "^0.7.0",

View File

@ -10,30 +10,41 @@ import { versionsAndTestEmoji } from './bin/versionsAndTestEmoji'
const dev = process.env.NODE_ENV !== 'production'
const svelte = dev ? hotSvelte : mainSvelte
// Build Database.js and Picker.js as separate modules at build times so that they are properly tree-shakeable.
// Most of this has to happen because customElements.define() has side effects
const baseConfig = {
plugins: [
resolve(),
cjs({
exclude: ['node_modules/lodash-es/**']
}),
cjs(),
json(),
replace({
'process.env.VERSIONS_AND_TEST_EMOJI': JSON.stringify(versionsAndTestEmoji)
}),
replace({
'../../../database/Database.js': './Database.js',
delimiters: ['', '']
}),
svelte({
css: true,
customElement: true,
dev,
preprocess: autoPreprocess()
})
],
external: [
'../../../database/Database.js'
]
}
const formats = ['es', 'cjs']
const entryPoints = [
{
input: './src/index.js',
output: 'index.js'
input: './src/svelte/components/Picker/Picker.svelte',
output: 'Picker.js'
},
{
input: './src/database/Database.js',
output: 'Database.js'
}
]

View File

@ -5,10 +5,10 @@ import { assertEmojiBaseData } from './utils/assertEmojiBaseData'
import { assertNumber } from './utils/assertNumber'
import { DEFAULT_DATA_SOURCE, DEFAULT_LOCALE } from './constants'
import { uniqEmoji } from './utils/uniqEmoji'
import { jsonChecksum } from '../svelte/utils/jsonChecksum'
import { jsonChecksum } from './utils/jsonChecksum'
import { warnOffline } from './utils/warnOffline'
export class Database {
export default class Database {
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE } = {}) {
this._dataSource = dataSource
this._locale = locale
@ -65,6 +65,10 @@ export class Database {
await this._idbEngine.loadData(emojiBaseData, url, eTag)
}
async ready () {
return this._readyPromise
}
async getEmojiByGroup (group) {
assertNumber(group)
await this._readyPromise

View File

@ -1,6 +1,13 @@
const requiredKeys = [
'annotation', 'emoji', 'emoticon', 'group',
'order', 'shortcodes', 'tags', 'version'
]
export function assertEmojiBaseData (emojiBaseData) {
if (!emojiBaseData || !Array.isArray(emojiBaseData) ||
!emojiBaseData.length || !emojiBaseData[0].emoji || !emojiBaseData[0].version) {
if (!emojiBaseData ||
!Array.isArray(emojiBaseData) ||
!emojiBaseData.length ||
requiredKeys.some(key => (!(key in emojiBaseData[0])))) {
throw new Error('Expected emojibase full (not compact) data, but data is in wrong format')
}
}

6
src/index.cjs.js Normal file
View File

@ -0,0 +1,6 @@
// Directly copied into dist/cjs/index.js
Object.defineProperty(exports, '__esModule', { value: true })
exports.Picker = require('./Picker.js')
exports.Database = require('./Database.js')

5
src/index.es.js Normal file
View File

@ -0,0 +1,5 @@
// Directly copied into dist/es/index.js
import Picker from './Picker.js'
import Database from './Database.js'
export { Picker, Database }

View File

@ -1,4 +1,4 @@
<svelte:options tag={null} />
<svelte:options tag="lite-emoji-picker" />
<section
class="lep-picker {resolvedDarkMode ? 'lep-dark' : 'lep-light'}"
aria-label={i18n.regionLabel}

View File

@ -1,15 +1,15 @@
/* global supportedZwjEmojis, emojiSupportLevel */
/* eslint-disable prefer-const,no-labels */
import Database from '../../../database/Database.js'
import i18n from '../../i18n/en.json'
import { categories } from '../../categories'
import { DEFAULT_LOCALE, DEFAULT_DATA_SOURCE } from '../../../database/constants'
import { Database } from '../../../database/Database'
import { MIN_SEARCH_TEXT_LENGTH, DEFAULT_NUM_COLUMNS } from '../../constants'
import { requestIdleCallback } from '../../utils/requestIdleCallback'
import { calculateTextWidth } from '../../utils/calculateTextWidth'
import { hasZwj } from '../../utils/hasZwj'
import { thunk } from '../../utils/thunk'
import { emojiSupportLevel, supportedZwjEmojis } from '../../utils/emojiSupport'
let database
let numColumns = DEFAULT_NUM_COLUMNS

View File

@ -1,4 +1,3 @@
<template src="./Picker.html"></template>
<style src="./Picker.scss" lang="scss"></style>
<script context="module" src="./Picker.module.js"></script>
<script src="./Picker.js"></script>

View File

@ -1,4 +1,4 @@
import { determineEmojiSupportLevel } from '../../utils/determineEmojiSupportLevel'
import { determineEmojiSupportLevel } from './determineEmojiSupportLevel'
// Check which emojis we know for sure aren't supported, based on Unicode version level
export const emojiSupportLevel = determineEmojiSupportLevel()
// determine which emojis containing ZWJ (zero width joiner) characters

View File

@ -7,23 +7,16 @@
.container {
height: 300px;
width: 400px;
overflow: hidden;
}
</style>
</head>
<body>
<h1>Ad-hoc test</h1>
<div class="container"></div>
<div class="container">
<lite-emoji-picker></lite-emoji-picker>
</div>
<script type="module">
import * as liteEmojiPicker from '../dist/es/index.js'
(async () => {
window.liteEmojiPicker = liteEmojiPicker
customElements.define('lite-emoji-picker', liteEmojiPicker.Picker)
const picker = document.createElement('lite-emoji-picker')
document.querySelector('.container').appendChild(picker)
})()
import '../dist/es/index.js'
</script>
</body>
</html>

View File

@ -404,7 +404,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
chalk@^2.0.0, chalk@^2.1.0:
chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@ -1950,6 +1950,16 @@ load-json-file@^2.0.0:
pify "^2.0.0"
strip-bom "^3.0.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"
load-json-file@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3"
@ -2049,6 +2059,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@ -2241,6 +2256,21 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
dependencies:
ansi-styles "^3.2.1"
chalk "^2.4.1"
cross-spawn "^6.0.5"
memorystream "^0.3.1"
minimatch "^3.0.4"
pidtree "^0.3.0"
read-pkg "^3.0.0"
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
npm-run-path@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@ -2517,6 +2547,13 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
dependencies:
pify "^3.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@ -2527,11 +2564,21 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
pidtree@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@ -2677,6 +2724,15 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^2.0.2:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@ -2956,6 +3012,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.6.1:
version "1.7.2"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -3148,6 +3209,14 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string.prototype.padend@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz#dc08f57a8010dc5c153550318f67e13adbb72ac3"
integrity sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
string.prototype.trimend@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"