perf: replace Svelte with vanilla JS (#381)

This commit is contained in:
Nolan Lawson 2023-12-17 12:28:46 -08:00 committed by GitHub
parent 750e8493e3
commit 56992858c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1766 additions and 1055 deletions

2
.gitignore vendored
View File

@ -123,8 +123,6 @@ dist
/trimEmojiData.js.map
/trimEmojiData.cjs
/trimEmojiData.cjs.map
/svelte.js
/svelte.js.map
/docs-tmp
/ts-tmp

View File

@ -59,15 +59,32 @@ Build the GitHub Pages docs site:
Some explanations of why the code is structured the way it is, in case it's confusing.
### Why is it one big Svelte component?
### Why a custom framework?
When you build Svelte components with `customElement: true`, it makes _each individual component_ into a web component. This can be bad for perf reasons (lots of repetition, [constructible stylesheets](https://wicg.github.io/construct-stylesheets/) aren't a thing yet, event and prop overhead) as well as correctness reasons (e.g. I want an `<li>` inside of a `<ul>`, not a `<custom-element>` with a shadow DOM and the `<li>` inside of it).
It was [a good learning exercise](https://nolanlawson.com/2023/12/02/lets-learn-how-modern-javascript-frameworks-work-by-building-one/), and it reduced the bundle size quite a bit to switch from Svelte to a custom framework. Plus, `emoji-picker-element` no longer needs to keep
up with breaking changes in Svelte or the tools in the Svelte ecosystem (e.g. Rollup and Jest plugins).
So for now: it's one big component.
### What are some of the quirks of the custom framework?
### Why use svelte-preprocess?
The framework mostly gets the job done, but I took a few shortcuts since we didn't need all the possible bells and whistles. Here is a brief description.
Since it's one big component, it's more readable if we split up the HTML/CSS/JS. Plus, we can lint the JS more easily that way. Plus, I like SCSS.
First, all the DOM nodes and update functions for those nodes are kept in-memory via a `WeakMap` where the key is the `state`. There's one `state` per instance of the `Picker.js` Svelte-esque component. So when the instance is GC'ed, everything related to the DOM and update functions should be GC'ed. (The exception is the global `parseCache`, which only contains the clone-able `template` and bindings for each unique `tokens` array, which is unique per `html` tag template literal. These templates/bindings never changes per component instance, so it makes sense to just parse once and cache them forever, in case the `<emoji-picker>` element itself is constantly unmounted and re-created.)
Second, I took a shortcut, which is that all unique (non-`<template>`) DOM nodes and update functions are keyed off of 1) the unique tokens for the tag template literal plus 2) a unique `key` from the `map` function (if it exists). These are only GC'ed when the whole `state` is GC'ed. So in the worst case, every DOM node for every emoji in the picker is kept in memory (e.g. if you click on every tab button), but this seemed like a reasonable tradeoff for simplicity, plus the perf improvement of avoiding re-rendering the same node when it's unchanged (this is especially important if the reactivity system is kind of chatty, and is constantly setting the same arrays over and over the framework just notices that all the `children` are the same objects and doesn't re-render). This also works because the `map`ed DOM nodes are not highly dynamic.
Third, all refs and event listeners are only bound once this just happens to work since most of the event listeners are hoisted (delegated) anyway.
Fourth, `map`ped iterations without a single top-level element are unsupported this makes updating iterations much easier, since I can just use `Element.replaceChildren()` instead of having to keep bookmark comment nodes or something.
Fifth, the reactivity system is really bare-bones and doesn't check for cycles or avoid wasteful re-renderings or anything. So there's a lot of guardrails to avoid setting the same object over and over to avoid infinite cycles or to avoid excessive re-renders.
Sixth, I tried to get fine-grained reactivity working but gave up, so basically the whole top-level `PickerTemplate.js` function is executed over and over again anytime anything changes. So there are guardrails in place to make sure this isn't expensive (e.g. the caching mechanisms described above).
There's also a long tail of things that aren't supported in the HTML parser, like funky characters like `<` and `=` inside of text nodes, which could confuse the parser (so I just don't support them).
Also, it's assumed that we're using some kind of minifier for the HTML tagged template literals it would be annoying to have to author `PickerTemplate.js` without any whitespace. So the parser doesn't support comments since those are assumed to be stripped out anyway.
That's about it, there are probably bugs in the framework if you tried to use it for something other than `emoji-picker-element`, but that's fine it only needs to support one component anyway.
### Why are the built JS files at the root of the project?
@ -77,4 +94,4 @@ I could also build a `pkg/` directory and copy the `package.json` into it (this
### Why build two separate bundles?
`picker.js` and `database.js` are designed to be independentally `import`-able. The only way to do this correctly with the right behavior from bundlers like Rollup and Webpack is to create two separate files. Otherwise the bundler would not be able to tree-shake `picker` from `database`.
`picker.js` and `database.js` are designed to be independently `import`-able. The only way to do this correctly with the right behavior from bundlers like Rollup and Webpack is to create two separate files. Otherwise the bundler would not be able to tree-shake `picker` from `database`.

View File

@ -827,17 +827,22 @@ The reason for this is that `Picker` automatically registers itself as a custom
### Within a Svelte project
`emoji-picker-element` is explicitly designed as a custom element, and won't work
as a direct Svelte component. However, if you're already using Svelte 3, then you
can avoid importing Svelte twice by using:
> [!WARNING]
> `emoji-picker-element` is no longer based on Svelte, so importing from `emoji-picker-element/svelte` is now deprecated.
Previously, `emoji-picker-element` was based on Svelte v3/v4, and you could do:
```js
import Picker from 'emoji-picker-element/svelte';
```
`svelte.js` is the same as `picker.js`, except it `import`s Svelte rather than bundling it.
The goal was to slightly reduce the bundle size by sharing common `svelte` imports.
While this option can reduce your bundle size, note that it only works with compatible Svelte versions. Currently Svelte v3 and v4 are supported.
This is still supported for backwards compatibility, but it is deprecated and just re-exports the Picker. Instead, do:
```js
import Picker from 'emoji-picker-element/picker';
```
## Data and offline

View File

@ -5,8 +5,8 @@ import { promisify } from 'node:util'
import prettyBytes from 'pretty-bytes'
import fs from 'node:fs/promises'
const MAX_SIZE_MIN = '42.7 kB'
const MAX_SIZE_MINGZ = '15 kB'
const MAX_SIZE_MIN = '37 kB'
const MAX_SIZE_MINGZ = '13 kB'
const FILENAME = './bundle.js'

View File

@ -0,0 +1,9 @@
import { minifyHTMLLiterals } from 'minify-html-literals'
export default {
processAsync (source, fileName) {
return minifyHTMLLiterals(source, {
fileName
})
}
}

View File

@ -1,5 +0,0 @@
import preprocess from 'svelte-preprocess'
export default {
preprocess: preprocess()
}

View File

@ -4,15 +4,9 @@ module.exports = {
'<rootDir>/test/spec/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
transform: {
'^.+\\.svelte$': ['svelte-jester', {
preprocess: './config/svelte.config.js',
compilerOptions: {
dev: false
}
}]
'^.*PickerTemplate.js$': './config/minifyHtmlInJest.js'
},
moduleFileExtensions: ['js', 'svelte'],
extensionsToTreatAsEsm: ['.svelte'],
moduleFileExtensions: ['js'],
testPathIgnorePatterns: ['node_modules'],
bail: true,
verbose: true,
@ -31,12 +25,6 @@ module.exports = {
branches: 100,
functions: 100,
lines: 100
},
'./src/picker/components/Picker/Picker.svelte': {
statements: 90,
branches: 85,
functions: 90,
lines: 90
}
}
}

View File

@ -62,8 +62,7 @@
"custom",
"element",
"web",
"component",
"svelte"
"component"
],
"author": "Nolan Lawson <nolan@nolanlawson.com>",
"license": "Apache-2.0",
@ -101,9 +100,9 @@
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.0",
"lodash-es": "^4.17.15",
"magic-string": "^0.30.5",
"markdown-table": "^3.0.2",
"markdown-toc": "^1.2.0",
"minify-html-literals": "^1.3.5",
"npm-run-all": "^4.1.5",
"playwright": "^1.40.1",
"pretty-bytes": "^6.1.1",
@ -111,7 +110,6 @@
"recursive-readdir": "^2.2.3",
"rollup": "^4.7.0",
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-svelte": "^7.1.6",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.69.5",
"shx": "^0.3.4",
@ -120,9 +118,6 @@
"stylelint": "^15.11.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-scss": "^5.3.1",
"svelte": "^4.2.8",
"svelte-jester": "^3.0.0",
"svelte-preprocess": "^5.1.1",
"svgo": "^3.0.5",
"tachometer": "^0.7.0",
"terser": "^5.26.0"
@ -151,11 +146,14 @@
"Event",
"fetch",
"getComputedStyle",
"Element",
"indexedDB",
"IDBKeyRange",
"Headers",
"HTMLElement",
"matchMedia",
"Node",
"NodeFilter",
"performance",
"ResizeObserver",
"Response",

View File

@ -1,31 +1,14 @@
import MagicString from 'magic-string'
import inject from '@rollup/plugin-inject'
import cjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import strip from '@rollup/plugin-strip'
import svelte from 'rollup-plugin-svelte'
import preprocess from 'svelte-preprocess'
import analyze from 'rollup-plugin-analyzer'
import { buildStyles } from './bin/buildStyles.js'
import { minifyHTMLLiterals } from 'minify-html-literals'
const { NODE_ENV, DEBUG } = process.env
const dev = NODE_ENV !== 'production'
const preprocessConfig = preprocess()
const origMarkup = preprocessConfig.markup
// minify the HTML by removing extra whitespace
// TODO: this is fragile, but it also results in a lot of bundlesize savings. let's find a better solution
preprocessConfig.markup = async function () {
const res = await origMarkup.apply(this, arguments)
// remove whitespace
res.code = res.code.replace(/([>}])\s+([<{])/sg, '$1$2')
return res
}
// Build Database.test.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 = {
@ -43,25 +26,18 @@ const baseConfig = {
delimiters: ['', ''],
preventAssignment: true
}),
svelte({
compilerOptions: {
dev,
discloseVersion: false
},
preprocess: preprocessConfig
}),
// make the svelte output slightly smaller
replace({
'options.anchor': 'undefined',
'options.context': 'undefined',
'options.customElement': 'undefined',
'options.hydrate': 'undefined',
'options.intro': 'undefined',
delimiters: ['', ''],
preventAssignment: true
}),
{
name: 'minify-html-in-tag-template-literals',
transform (content, id) {
if (id.includes('PickerTemplate.js')) {
return minifyHTMLLiterals(content, {
fileName: id
})
}
}
},
strip({
include: ['**/*.js', '**/*.svelte'],
include: ['**/*.js'],
functions: [
(!dev && !process.env.PERF) && 'performance.*',
!dev && 'console.log'
@ -78,18 +54,7 @@ const baseConfig = {
const entryPoints = [
{
input: './src/picker/PickerElement.js',
output: './picker.js',
plugins: [
// Replace newer syntax in Svelte v4 to avoid breaking iOS <13.4
// https://github.com/nolanlawson/emoji-picker-element/pull/379
replace({
'array_like_or_iterator?.length': 'array_like_or_iterator && array_like_or_iterator.length',
'$$ = undefined;': '', // not necessary to initialize class prop to undefined
'$$set = undefined;': '', // not necessary to initialize class prop to undefined
delimiters: ['', ''],
preventAssignment: true
})
]
output: './picker.js'
},
{
input: './src/database/Database.js',
@ -103,42 +68,10 @@ const entryPoints = [
input: './src/trimEmojiData.js',
output: './trimEmojiData.cjs',
format: 'cjs'
},
{
input: './src/picker/PickerElement.js',
output: './svelte.js',
external: ['svelte', 'svelte/internal'],
// TODO: drop Svelte v3 support
// ensure_array_like was added in Svelte v4 - we shim it to avoid breaking Svelte v3 users
plugins: [
{
name: 'svelte-v3-compat',
transform (source) {
const magicString = new MagicString(source)
magicString.replaceAll('ensure_array_like(', 'ensure_array_like_shim(')
return {
code: magicString.toString(),
map: magicString.generateMap()
}
}
},
inject({
ensure_array_like_shim: [
'../../../../shims/svelte-v3-shim.js',
'ensure_array_like_shim'
]
})
],
onwarn (warning) {
if (!warning.message.includes('ensure_array_like')) { // intentionally ignore warning for unused import
console.warn(warning.message)
}
}
}
]
export default entryPoints.map(({ input, output, format = 'es', external = [], plugins = [], onwarn }) => {
export default entryPoints.map(({ input, output, format = 'es', external = [], onwarn }) => {
return {
input,
output: {
@ -148,7 +81,7 @@ export default entryPoints.map(({ input, output, format = 'es', external = [], p
exports: 'auto'
},
external: [...baseConfig.external, ...external],
plugins: [...baseConfig.plugins, ...plugins],
plugins: baseConfig.plugins,
onwarn
}
})

View File

@ -1,9 +0,0 @@
// TODO: drop Svelte v3 support
// ensure_array_like was added in Svelte v4 - we shim it to avoid breaking Svelte v3 users
// this code is copied from svelte v4
/* eslint-disable camelcase */
export function ensure_array_like_shim (array_like_or_iterator) {
return (array_like_or_iterator && array_like_or_iterator.length !== undefined)
? array_like_or_iterator
: Array.from(array_like_or_iterator)
}

View File

@ -1,8 +1,9 @@
import SveltePicker from './components/Picker/Picker.svelte'
import { createRoot } from './components/Picker/Picker.js'
import { DEFAULT_DATA_SOURCE, DEFAULT_LOCALE } from '../database/constants'
import { DEFAULT_CATEGORY_SORTING, DEFAULT_SKIN_TONE_EMOJI, FONT_FAMILY } from './constants'
import enI18n from './i18n/en.js'
import Database from './ImportedDatabase'
import { queueMicrotask } from './utils/queueMicrotask.js'
const PROPS = [
'customEmoji',
@ -51,17 +52,14 @@ export default class PickerElement extends HTMLElement {
// The _cmp may be defined if the component was immediately disconnected and then reconnected. In that case,
// do nothing (preserve the state)
if (!this._cmp) {
this._cmp = new SveltePicker({
target: this.shadowRoot,
props: this._ctx
})
this._cmp = createRoot(this.shadowRoot, this._ctx)
}
}
disconnectedCallback () {
// Check in a microtask if the element is still connected. If so, treat this as a "move" rather than a disconnect
// Inspired by Vue: https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue
Promise.resolve().then(() => {
queueMicrotask(() => {
// this._cmp may be defined if connect-disconnect-connect-disconnect occurs synchronously
if (!this.isConnected && this._cmp) {
this._cmp.$destroy()
@ -110,7 +108,7 @@ export default class PickerElement extends HTMLElement {
// Update the Database in one microtask if the locale/dataSource change. We do one microtask
// so we don't create two Databases if e.g. both the locale and the dataSource change
_dbFlush () {
Promise.resolve().then(() => (
queueMicrotask(() => (
this._dbCreate()
))
}

View File

@ -1,197 +0,0 @@
<svelte:options customElement={null} /><section
class="picker"
aria-label={i18n.regionLabel}
style={pickerStyle}
bind:this={rootElement}>
<!-- using a spacer div because this allows us to cover up the skintone picker animation -->
<div class="pad-top"></div>
<div class="search-row">
<div class="search-wrapper">
<!-- no need for aria-haspopup=listbox, it's the default for role=combobox
https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/combobox/aria1.1pattern/listbox-combo.html
-->
<input
id="search"
class="search"
type="search"
role="combobox"
enterkeyhint="search"
placeholder={i18n.searchLabel}
autocapitalize="none"
autocomplete="off"
spellcheck="true"
aria-expanded={!!(searchMode && currentEmojis.length)}
aria-controls="search-results"
aria-describedby="search-description"
aria-autocomplete="list"
aria-activedescendant={activeSearchItemId ? `emo-${activeSearchItemId}` : ''}
bind:value={rawSearchText}
on:keydown={onSearchKeydown}
>
<label class="sr-only" for="search">{i18n.searchLabel}</label>
<span id="search-description" class="sr-only">{i18n.searchDescription}</span>
</div>
<!-- For the pattern used for the skintone dropdown, see:
https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
The one case where we deviate from the example is that we move focus from the button to the
listbox. (The example uses a combobox, so it's not exactly the same.) This was tested in NVDA and VoiceOver.
Note that Svelte's a11y checker will warn if the listbox does not have a tabindex.
https://github.com/sveltejs/svelte/blob/3bc791b/site/content/docs/06-accessibility-warnings.md#a11y-aria-activedescendant-has-tabindex
-->
<div class="skintone-button-wrapper {skinTonePickerExpandedAfterAnimation ? 'expanded' : ''}">
<button id="skintone-button"
class="emoji {skinTonePickerExpanded ? 'hide-focus' : ''}"
aria-label={skinToneButtonLabel}
title={skinToneButtonLabel}
aria-describedby="skintone-description"
aria-haspopup="listbox"
aria-expanded={skinTonePickerExpanded}
aria-controls="skintone-list"
on:click={onClickSkinToneButton}>
{skinToneButtonText}
</button>
</div>
<span id="skintone-description" class="sr-only">{i18n.skinToneDescription}</span>
<div id="skintone-list"
class="skintone-list hide-focus {skinTonePickerExpanded ? '' : 'hidden no-animate'}"
style="transform:translateY({ skinTonePickerExpanded ? 0 : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))'})"
role="listbox"
aria-label={i18n.skinTonesLabel}
aria-activedescendant="skintone-{activeSkinTone}"
aria-hidden={!skinTonePickerExpanded}
tabindex="-1"
on:focusout={onSkinToneOptionsFocusOut}
on:click={onSkinToneOptionsClick}
on:keydown={onSkinToneOptionsKeydown}
on:keyup={onSkinToneOptionsKeyup}
bind:this={skinToneDropdown}>
{#each skinTones as skinTone, i (skinTone)}
<div id="skintone-{i}"
class="emoji {i === activeSkinTone ? 'active' : ''}"
aria-selected={i === activeSkinTone}
role="option"
title={i18n.skinTones[i]}
aria-label={i18n.skinTones[i]}>
{skinTone}
</div>
{/each}
</div>
</div>
<!-- this is interactive because of keydown; it doesn't really need focus -->
<!-- svelte-ignore a11y-interactive-supports-focus -->
<div class="nav"
role="tablist"
style="grid-template-columns: repeat({groups.length}, 1fr)"
aria-label={i18n.categoriesLabel}
on:keydown={onNavKeydown}>
{#each groups as group (group.id)}
<button role="tab"
class="nav-button"
aria-controls="tab-{group.id}"
aria-label={i18n.categories[group.name]}
aria-selected={!searchMode && currentGroup.id === group.id}
title={i18n.categories[group.name]}
on:click={() => onNavClick(group)}>
<div class="nav-emoji emoji">
{group.emoji}
</div>
</button>
{/each}
</div>
<div class="indicator-wrapper">
<div class="indicator"
style="transform: translateX({(isRtl ? -1 : 1) * currentGroupIndex * 100}%)">
</div>
</div>
<div class="message {message ? '' : 'gone'}"
role="alert"
aria-live="polite">
{message}
</div>
<!-- The tabindex=0 is so people can scroll up and down with the keyboard. The element has a role and a label, so I
feel it's appropriate to have the tabindex. -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- This on:click is a delegated click listener -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="tabpanel {(!databaseLoaded || message) ? 'gone': ''}"
role={searchMode ? 'region' : 'tabpanel'}
aria-label={searchMode ? i18n.searchResultsLabel : i18n.categories[currentGroup.name]}
id={searchMode ? '' : `tab-${currentGroup.id}`}
tabindex="0"
on:click={onEmojiClick}
bind:this={tabpanelElement}
>
<div use:calculateEmojiGridStyle>
{#each currentEmojisWithCategories as emojiWithCategory, i (emojiWithCategory.category)}
<div
id="menu-label-{i}"
class="category {currentEmojisWithCategories.length === 1 && currentEmojisWithCategories[0].category === '' ? 'gone' : ''}"
aria-hidden="true">
<!-- This logic is a bit complicated in order to avoid a flash of the word "Custom" while switching
from a tabpanel with custom emoji to a regular group. I.e. we don't want it to suddenly flash
from "Custom" to "Smileys and emoticons" when you click the second nav button. The easiest
way to repro this is to add an artificial delay to the IndexedDB operations. -->
{
searchMode ?
i18n.searchResultsLabel : (
emojiWithCategory.category ?
emojiWithCategory.category : (
currentEmojisWithCategories.length > 1 ?
i18n.categories.custom :
i18n.categories[currentGroup.name]
)
)
}
</div>
<div class="emoji-menu"
role={searchMode ? 'listbox' : 'menu'}
aria-labelledby="menu-label-{i}"
id={searchMode ? 'search-results' : ''}>
{#each emojiWithCategory.emojis as emoji, i (emoji.id)}
<button role={searchMode ? 'option' : 'menuitem'}
aria-selected={searchMode ? i == activeSearchItem : ''}
aria-label={labelWithSkin(emoji, currentSkinTone)}
title={titleForEmoji(emoji)}
class="emoji {searchMode && i === activeSearchItem ? 'active' : ''}"
id="emo-{emoji.id}">
{#if emoji.unicode}
{unicodeWithSkin(emoji, currentSkinTone)}
{:else}
<img class="custom-emoji" src={emoji.url} alt="" loading="lazy" />
{/if}
</button>
{/each}
</div>
{/each}
</div>
</div>
<!-- This on:click is a delegated click listener -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-interactive-supports-focus -->
<div class="favorites emoji-menu {message ? 'gone': ''}"
role="menu"
aria-label={i18n.favoritesLabel}
style="padding-inline-end: {scrollbarWidth}px"
on:click={onEmojiClick}>
<!-- The reason the emoji logic below is largely duplicated is because it turns out we get a smaller
bundle size from just repeating it twice, rather than creating a second Svelte component. -->
{#each currentFavorites as emoji, i (emoji.id)}
<button role="menuitem"
aria-label={labelWithSkin(emoji, currentSkinTone)}
title={titleForEmoji(emoji)}
class="emoji"
id="fav-{emoji.id}">
{#if emoji.unicode}
{unicodeWithSkin(emoji, currentSkinTone)}
{:else}
<img class="custom-emoji" src={emoji.url} alt="" loading="lazy" />
{/if}
</button>
{/each}
</div>
<!-- This serves as a baseline emoji for measuring against and determining emoji support -->
<button aria-hidden="true" tabindex="-1" class="abs-pos hidden emoji" bind:this={baselineEmoji}>😀</button>
</section>

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
<template src="./Picker.html"></template>
<script src="./Picker.js"></script>

View File

@ -0,0 +1,258 @@
import { createFramework } from './framework.js'
export function render (container, state, helpers, events, actions, refs, abortSignal, firstRender) {
const { labelWithSkin, titleForEmoji, unicodeWithSkin } = helpers
const { html, map } = createFramework(state)
function emojiList (emojis, searchMode, prefix) {
return map(emojis, (emoji, i) => {
return html`
<button role="${searchMode ? 'option' : 'menuitem'}"
aria-selected="${state.searchMode ? i === state.activeSearchItem : ''}"
aria-label="${labelWithSkin(emoji, state.currentSkinTone)}"
title="${titleForEmoji(emoji)}"
class="emoji ${searchMode && i === state.activeSearchItem ? 'active' : ''}"
id=${`${prefix}-${emoji.id}`}>
${
emoji.unicode
? unicodeWithSkin(emoji, state.currentSkinTone)
: html`<img class="custom-emoji" src="${emoji.url}" alt="" loading="lazy"/>`
}
</button>
`
// It's important for the cache key to be unique based on the prefix, because the framework caches based on the
// unique tokens + cache key, and the same emoji may be used in the tab as well as in the fav bar
}, emoji => `${prefix}-${emoji.id}`)
}
const section = () => {
return html`
<section
data-ref="rootElement"
class="picker"
aria-label="${state.i18n.regionLabel}"
style="${state.pickerStyle}">
<!-- using a spacer div because this allows us to cover up the skintone picker animation -->
<div class="pad-top"></div>
<div class="search-row">
<div class="search-wrapper">
<!-- no need for aria-haspopup=listbox, it's the default for role=combobox
https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/combobox/aria1.1pattern/listbox-combo.html
-->
<input
id="search"
class="search"
type="search"
role="combobox"
enterkeyhint="search"
placeholder="${state.i18n.searchLabel}"
autocapitalize="none"
autocomplete="off"
spellcheck="true"
aria-expanded="${!!(state.searchMode && state.currentEmojis.length)}"
aria-controls="search-results"
aria-describedby="search-description"
aria-autocomplete="list"
aria-activedescendant="${state.activeSearchItemId ? `emo-${state.activeSearchItemId}` : ''}"
data-ref="searchElement"
data-on-input="onSearchInput"
data-on-keydown="onSearchKeydown"
></input>
<label class="sr-only" for="search">${state.i18n.searchLabel}</label>
<span id="search-description" class="sr-only">${state.i18n.searchDescription}</span>
</div>
<!-- For the pattern used for the skintone dropdown, see:
https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
The one case where we deviate from the example is that we move focus from the button to the
listbox. (The example uses a combobox, so it's not exactly the same.) This was tested in NVDA and VoiceOver. -->
<div class="skintone-button-wrapper ${state.skinTonePickerExpandedAfterAnimation ? 'expanded' : ''}">
<button id="skintone-button"
class="emoji ${state.skinTonePickerExpanded ? 'hide-focus' : ''}"
aria-label="${state.skinToneButtonLabel}"
title="${state.skinToneButtonLabel}"
aria-describedby="skintone-description"
aria-haspopup="listbox"
aria-expanded="${state.skinTonePickerExpanded}"
aria-controls="skintone-list"
data-on-click="onClickSkinToneButton">
${state.skinToneButtonText}
</button>
</div>
<span id="skintone-description" class="sr-only">${state.i18n.skinToneDescription}</span>
<div
data-ref="skinToneDropdown"
id="skintone-list"
class="skintone-list hide-focus ${state.skinTonePickerExpanded ? '' : 'hidden no-animate'}"
style="transform:translateY(${state.skinTonePickerExpanded ? 0 : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))'})"
role="listbox"
aria-label="${state.i18n.skinTonesLabel}"
aria-activedescendant="skintone-${state.activeSkinTone}"
aria-hidden="${!state.skinTonePickerExpanded}"
tabIndex="-1"
data-on-focusout="onSkinToneOptionsFocusOut"
data-on-click="onSkinToneOptionsClick"
data-on-keydown="onSkinToneOptionsKeydown"
data-on-keyup="onSkinToneOptionsKeyup">
${
map(state.skinTones, (skinTone, i) => {
return html`
<div id="skintone-${i}"
class="emoji ${i === state.activeSkinTone ? 'active' : ''}"
aria-selected="${i === state.activeSkinTone}"
role="option"
title="${state.i18n.skinTones[i]}"
aria-label="${state.i18n.skinTones[i]}">
${skinTone}
</div>
`
}, skinTone => skinTone)
}
</div>
</div>
<!-- this is interactive because of keydown; it doesn't really need focus -->
<div class="nav"
role="tablist"
style="grid-template-columns: repeat(${state.groups.length}, 1fr)"
aria-label="${state.i18n.categoriesLabel}"
data-on-keydown="onNavKeydown"
data-on-click="onNavClick"
>
${
map(state.groups, (group) => {
return html`
<button role="tab"
class="nav-button"
aria-controls="tab-${group.id}"
aria-label="${state.i18n.categories[group.name]}"
aria-selected="${!state.searchMode && state.currentGroup.id === group.id}"
title="${state.i18n.categories[group.name]}"
data-group-id=${group.id}
>
<div class="nav-emoji emoji">
${group.emoji}
</div>
</button>
`
}, group => group.id)
}
</div>
<div class="indicator-wrapper">
<!-- Note we cannot test RTL in Jest because of lack of getComputedStyle() -->
<div class="indicator"
style="transform: translateX(${(/* istanbul ignore next */ (state.isRtl ? -1 : 1)) * state.currentGroupIndex * 100}%)">
</div>
</div>
<div class="message ${state.message ? '' : 'gone'}"
role="alert"
aria-live="polite">
${state.message}
</div>
<!--The tabindex=0 is so people can scroll up and down with the keyboard. The element has a role and a label, so I
feel it's appropriate to have the tabindex.
This on:click is a delegated click listener -->
<div data-ref="tabpanelElement" class="tabpanel ${(!state.databaseLoaded || state.message) ? 'gone' : ''}"
role="${state.searchMode ? 'region' : 'tabpanel'}"
aria-label="${state.searchMode ? state.i18n.searchResultsLabel : state.i18n.categories[state.currentGroup.name]}"
id="${state.searchMode ? '' : `tab-${state.currentGroup.id}`}"
tabIndex="0"
data-on-click="onEmojiClick"
>
<div data-action="calculateEmojiGridStyle">
${
map(state.currentEmojisWithCategories, (emojiWithCategory, i) => {
return html`
<!-- wrapper div so there's one top level element for this loop -->
<div>
<div
id="menu-label-${i}"
class="category ${state.currentEmojisWithCategories.length === 1 && state.currentEmojisWithCategories[0].category === '' ? 'gone' : ''}"
aria-hidden="true">
<!-- This logic is a bit complicated in order to avoid a flash of the word "Custom" while switching
from a tabpanel with custom emoji to a regular group. I.e. we don't want it to suddenly flash
from "Custom" to "Smileys and emoticons" when you click the second nav button. The easiest
way to repro this is to add an artificial delay to the IndexedDB operations. -->
${
state.searchMode
? state.i18n.searchResultsLabel
: (
emojiWithCategory.category
? emojiWithCategory.category
: (
state.currentEmojisWithCategories.length > 1
? state.i18n.categories.custom
: state.i18n.categories[state.currentGroup.name]
)
)
}
</div>
<div class="emoji-menu"
role="${state.searchMode ? 'listbox' : 'menu'}"
aria-labelledby="menu-label-${i}"
id=${state.searchMode ? 'search-results' : ''}>
${
emojiList(emojiWithCategory.emojis, state.searchMode, /* prefix */ 'emo')
}
</div>
</div>
`
}, emojiWithCategory => emojiWithCategory.category)
}
</div>
</div>
<!-- This on:click is a delegated click listener -->
<div class="favorites emoji-menu ${state.message ? 'gone' : ''}"
role="menu"
aria-label="${state.i18n.favoritesLabel}"
style="padding-inline-end: ${`${state.scrollbarWidth}px`}"
data-on-click="onEmojiClick">
${
emojiList(state.currentFavorites, /* searchMode */ false, /* prefix */ 'fav')
}
</div>
<!-- This serves as a baseline emoji for measuring against and determining emoji support -->
<button data-ref="baselineEmoji" aria-hidden="true" tabindex="-1" class="abs-pos hidden emoji baseline-emoji">
😀
</button>
</section>
`
}
const rootDom = section()
if (firstRender) { // not a re-render
container.appendChild(rootDom)
// we only bind events/refs/actions once - there is no need to find them again given this component structure
// helper for traversing the dom, finding elements by an attribute, and getting the attribute value
const forElementWithAttribute = (attributeName, callback) => {
for (const element of container.querySelectorAll(`[${attributeName}]`)) {
callback(element, element.getAttribute(attributeName))
}
}
// bind events
for (const eventName of ['click', 'focusout', 'input', 'keydown', 'keyup']) {
forElementWithAttribute(`data-on-${eventName}`, (element, listenerName) => {
element.addEventListener(eventName, events[listenerName])
})
}
// find refs
forElementWithAttribute('data-ref', (element, ref) => {
refs[ref] = element
})
// set up actions
forElementWithAttribute('data-action', (element, action) => {
actions[action](element)
})
// destroy/abort logic
abortSignal.addEventListener('abort', () => {
container.removeChild(rootDom)
})
}
}

View File

@ -0,0 +1,314 @@
import { getFromMap, parseTemplate, toString } from './utils.js'
const parseCache = new WeakMap()
const domInstancesCache = new WeakMap()
const unkeyedSymbol = Symbol('un-keyed')
// for debugging
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
window.parseCache = parseCache
window.domInstancesCache = domInstancesCache
}
// Not supported in Safari <=13
const hasReplaceChildren = 'replaceChildren' in Element.prototype
function replaceChildren (parentNode, newChildren) {
/* istanbul ignore else */
if (hasReplaceChildren) {
parentNode.replaceChildren(...newChildren)
} else { // minimal polyfill for Element.prototype.replaceChildren
parentNode.innerHTML = ''
parentNode.append(...newChildren)
}
}
function doChildrenNeedRerender (parentNode, newChildren) {
let oldChild = parentNode.firstChild
let oldChildrenCount = 0
// iterate using firstChild/nextSibling because browsers use a linked list under the hood
while (oldChild) {
const newChild = newChildren[oldChildrenCount]
// check if the old child and new child are the same
if (newChild !== oldChild) {
return true
}
oldChild = oldChild.nextSibling
oldChildrenCount++
}
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && oldChildrenCount !== parentNode.children.length) {
throw new Error('parentNode.children.length is different from oldChildrenCount, it should not be')
}
// if new children length is different from old, we must re-render
return oldChildrenCount !== newChildren.length
}
function patchChildren (newChildren, instanceBinding) {
const { targetNode } = instanceBinding
let { targetParentNode } = instanceBinding
let needsRerender = false
if (targetParentNode) { // already rendered once
needsRerender = doChildrenNeedRerender(targetParentNode, newChildren)
} else { // first render of list
needsRerender = true
instanceBinding.targetNode = undefined // placeholder comment not needed anymore, free memory
instanceBinding.targetParentNode = targetParentNode = targetNode.parentNode
}
// avoid re-rendering list if the dom nodes are exactly the same before and after
if (needsRerender) {
replaceChildren(targetParentNode, newChildren)
}
}
function patch (expressions, instanceBindings) {
for (const instanceBinding of instanceBindings) {
const {
targetNode,
currentExpression,
binding: {
expressionIndex,
attributeName,
attributeValuePre,
attributeValuePost
}
} = instanceBinding
const expression = expressions[expressionIndex]
if (currentExpression === expression) {
// no need to update, same as before
continue
}
instanceBinding.currentExpression = expression
if (attributeName) { // attribute replacement
targetNode.setAttribute(attributeName, attributeValuePre + toString(expression) + attributeValuePost)
} else { // text node / child element / children replacement
let newNode
if (Array.isArray(expression)) { // array of DOM elements produced by tag template literals
patchChildren(expression, instanceBinding)
} else if (expression instanceof Element) { // html tag template returning a DOM element
newNode = expression
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && newNode === targetNode) {
// it seems impossible for the framework to get into this state, may as well assert on it
// worst case scenario is we lose focus if we call replaceWith on the same node
throw new Error('the newNode and targetNode are the same, this should never happen')
}
targetNode.replaceWith(newNode)
} else { // primitive - string, number, etc
if (targetNode.nodeType === Node.TEXT_NODE) { // already transformed into a text node
// nodeValue is faster than textContent supposedly https://www.youtube.com/watch?v=LY6y3HbDVmg
targetNode.nodeValue = toString(expression)
} else { // replace comment or whatever was there before with a text node
newNode = document.createTextNode(toString(expression))
targetNode.replaceWith(newNode)
}
}
if (newNode) {
instanceBinding.targetNode = newNode
}
}
}
}
function parse (tokens) {
let htmlString = ''
let withinTag = false
let withinAttribute = false
let elementIndexCounter = -1 // depth-first traversal order
const elementsToBindings = new Map()
const elementIndexes = []
for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i]
htmlString += token
if (i === len - 1) {
break // no need to process characters - no more expressions to be found
}
for (let j = 0; j < token.length; j++) {
const char = token.charAt(j)
switch (char) {
case '<': {
const nextChar = token.charAt(j + 1)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !/[/a-z]/.test(nextChar)) {
// we don't need to support comments ('<!') because we always use html-minify-literals
// also we don't support '<' inside tags, e.g. '<div> 2 < 3 </div>'
throw new Error('framework currently only supports a < followed by / or a-z')
}
if (nextChar === '/') { // closing tag
// leaving an element
elementIndexes.pop()
} else { // not a closing tag
withinTag = true
elementIndexes.push(++elementIndexCounter)
}
break
}
case '>': {
withinTag = false
withinAttribute = false
break
}
case '=': {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !withinTag) {
// we don't currently support '=' anywhere but inside a tag, e.g.
// we don't support '<div>2 + 2 = 4</div>'
throw new Error('framework currently does not support = anywhere but inside a tag')
}
withinAttribute = true
break
}
}
}
const elementIndex = elementIndexes[elementIndexes.length - 1]
const bindings = getFromMap(elementsToBindings, elementIndex, () => [])
let attributeName
let attributeValuePre
let attributeValuePost
if (withinAttribute) {
// I never use single-quotes for attribute values in HTML, so just support double-quotes or no-quotes
const match = /(\S+)="?([^"=]*)$/.exec(token)
attributeName = match[1]
attributeValuePre = match[2]
attributeValuePost = /^[^">]*/.exec(tokens[i + 1])[0]
}
const binding = {
attributeName,
attributeValuePre,
attributeValuePost,
expressionIndex: i
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// remind myself that this object is supposed to be immutable
Object.freeze(binding)
}
bindings.push(binding)
// add a placeholder comment that we can find later
htmlString += (!withinTag && !withinAttribute) ? `<!--${bindings.length - 1}-->` : ''
}
const template = parseTemplate(htmlString)
return {
template,
elementsToBindings
}
}
function findPlaceholderComment (element, bindingId) {
// If we had a lot of placeholder comments to find, it would make more sense to build up a map once
// rather than search the DOM every time. But it turns out that we always only have one child,
// and it's the comment node, so searching every time is actually faster.
let childNode = element.firstChild
while (childNode) {
// Note that minify-html-literals has already removed all non-framework comments
// So we just need to look for comments that have exactly the bindingId as its text content
if (childNode.nodeType === Node.COMMENT_NODE && childNode.nodeValue === toString(bindingId)) {
return childNode
}
childNode = childNode.nextSibling
}
}
function traverseAndSetupBindings (dom, elementsToBindings) {
const instanceBindings = []
// traverse dom
const treeWalker = document.createTreeWalker(dom, NodeFilter.SHOW_ELEMENT)
let element = dom
let elementIndex = -1
do {
const bindings = elementsToBindings.get(++elementIndex)
if (bindings) {
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
const targetNode = binding.attributeName
? element // attribute binding, just use the element itself
: findPlaceholderComment(element, i) // not an attribute binding, so has a placeholder comment
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !targetNode) {
throw new Error('targetNode should not be undefined')
}
const instanceBinding = {
binding,
targetNode,
targetParentNode: undefined,
currentExpression: undefined
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// remind myself that this object is supposed to be monomorphic (for better JS engine perf)
Object.seal(instanceBinding)
}
instanceBindings.push(instanceBinding)
}
}
} while ((element = treeWalker.nextNode()))
return instanceBindings
}
function parseHtml (tokens) {
// All templates and bound expressions are unique per tokens array
const { template, elementsToBindings } = getFromMap(parseCache, tokens, () => parse(tokens))
// When we parseHtml, we always return a fresh DOM instance ready to be updated
const dom = template.cloneNode(true).content.firstElementChild
const instanceBindings = traverseAndSetupBindings(dom, elementsToBindings)
return function updateDomInstance (expressions) {
patch(expressions, instanceBindings)
return dom
}
}
export function createFramework (state) {
const domInstances = getFromMap(domInstancesCache, state, () => new Map())
let domInstanceCacheKey = unkeyedSymbol
function html (tokens, ...expressions) {
// Each unique lexical usage of map() is considered unique due to the html`` tagged template call it makes,
// which has lexically unique tokens. The unkeyed symbol is just used for html`` usage outside of a map().
const domInstancesForTokens = getFromMap(domInstances, tokens, () => new Map())
const updateDomInstance = getFromMap(domInstancesForTokens, domInstanceCacheKey, () => parseHtml(tokens))
return updateDomInstance(expressions) // update with expressions
}
function map (array, callback, keyFunction) {
return array.map((item, index) => {
const originalCacheKey = domInstanceCacheKey
domInstanceCacheKey = keyFunction(item)
try {
return callback(item, index)
} finally {
domInstanceCacheKey = originalCacheKey
}
})
}
return { map, html }
}

View File

@ -0,0 +1,103 @@
import { queueMicrotask } from '../../utils/queueMicrotask.js'
export function createState (abortSignal) {
let destroyed = false
let currentObserver
const propsToObservers = new Map()
const dirtyObservers = new Set()
let queued
let recursionDepth = 0
const MAX_RECURSION_DEPTH = 30
const flush = () => {
if (destroyed) {
return
}
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && recursionDepth === MAX_RECURSION_DEPTH) {
throw new Error('max recursion depth, you probably didn\'t mean to do this')
}
const observersToRun = [...dirtyObservers]
dirtyObservers.clear() // clear before running to force any new updates to run in another tick of the loop
try {
for (const observer of observersToRun) {
observer()
}
} finally {
queued = false
if (dirtyObservers.size) { // new updates, queue another one
recursionDepth++
queued = true
queueMicrotask(flush)
}
}
}
const state = new Proxy({}, {
get (target, prop) {
// console.log('reactivity: get', prop)
if (currentObserver) {
let observers = propsToObservers.get(prop)
if (!observers) {
observers = new Set()
propsToObservers.set(prop, observers)
}
observers.add(currentObserver)
}
return target[prop]
},
set (target, prop, newValue) {
// console.log('reactivity: set', prop, newValue)
target[prop] = newValue
const observers = propsToObservers.get(prop)
if (observers) {
for (const observer of observers) {
dirtyObservers.add(observer)
}
if (!queued) {
recursionDepth = 0
queued = true
queueMicrotask(flush)
}
}
return true
}
})
const createEffect = (callback) => {
const runnable = () => {
const oldObserver = currentObserver
currentObserver = runnable
try {
return callback()
} finally {
currentObserver = oldObserver
}
}
return runnable()
}
// for debugging
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
window.state = state
}
// destroy logic
abortSignal.addEventListener('abort', () => {
destroyed = true
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
delete window.state
}
})
return {
state,
createEffect
}
}

View File

@ -0,0 +1,25 @@
export function getFromMap (cache, key, func) {
let cached = cache.get(key)
if (!cached) {
cached = func()
cache.set(key, cached)
}
return cached
}
export function toString (value) {
return '' + value
}
export function parseTemplate (htmlString) {
const template = document.createElement('template')
template.innerHTML = htmlString
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production') {
if (template.content.children.length !== 1) {
throw new Error('only 1 child allowed for now')
}
}
return template
}

View File

@ -1,5 +1,5 @@
// via https://unpkg.com/browse/emojibase-data@6.0.0/meta/groups.json
const allGroups = [
export const allGroups = [
[-1, '✨', 'custom'],
[0, '😀', 'smileys-emotion'],
[1, '👋', 'people-body'],
@ -13,4 +13,3 @@ const allGroups = [
].map(([id, emoji, name]) => ({ id, emoji, name }))
export const groups = allGroups.slice(1)
export const customGroup = allGroups[0]

View File

@ -0,0 +1,12 @@
// Compare two arrays, with a function called on each item in the two arrays that returns true if the items are equal
export function arraysAreEqualByFunction (left, right, areEqualFunc) {
if (left.length !== right.length) {
return false
}
for (let i = 0; i < left.length; i++) {
if (!areEqualFunc(left[i], right[i])) {
return false
}
}
return true
}

View File

@ -0,0 +1,3 @@
/* istanbul ignore next */
const qM = typeof queueMicrotask === 'function' ? queueMicrotask : callback => Promise.resolve().then(callback)
export { qM as queueMicrotask }

View File

@ -4,7 +4,8 @@
// https://github.com/sveltejs/svelte/issues/6521
// Also note tabpanelElement can be null if the element is disconnected immediately after connected
export function resetScrollTopIfPossible (element) {
if (element) {
/* istanbul ignore else */
if (element) { // Makes me nervous not to have this `if` guard
element.scrollTop = 0
}
}

View File

@ -11,7 +11,7 @@ export const resetResizeObserverSupported = () => {
resizeObserverSupported = typeof ResizeObserver === 'function'
}
export function calculateWidth (node, onUpdate) {
export function calculateWidth (node, abortSignal, onUpdate) {
let resizeObserver
if (resizeObserverSupported) {
resizeObserver = new ResizeObserver(entries => (
@ -25,11 +25,10 @@ export function calculateWidth (node, onUpdate) {
}
// cleanup function (called on destroy)
return {
destroy () {
if (resizeObserver) {
resizeObserver.disconnect()
}
abortSignal.addEventListener('abort', () => {
if (resizeObserver) {
console.log('ResizeObserver destroyed')
resizeObserver.disconnect()
}
}
})
}

4
svelte.js Normal file
View File

@ -0,0 +1,4 @@
console.warn('Importing emoji-picker-element from "emoji-picker-element/svelte" is deprecated. ' +
'Instead, import from "emoji-picker-element" or "emoji-picker-element/picker".')
import picker from './picker.js'
export default picker

View File

@ -178,6 +178,16 @@ describe('Picker tests', () => {
await waitFor(() => expect(queryAllByRole('listbox', { name: 'Skin tones' })).toHaveLength(0))
})
test('Click skintone button while picker is open', async () => {
// this should not be possible since the picker covers the button when it's open,
// but this is for test coverage, and just to be safe
await openSkintoneListbox(container)
await fireEvent.click(getByRole('button', { name: /Choose a skin tone/ }))
// listbox closes
await waitFor(() => expect(queryAllByRole('listbox', { name: 'Skin tones' })).toHaveLength(0))
})
test('nav keyboard test', async () => {
getByRole('tab', { name: 'Smileys and emoticons', selected: true }).focus()
@ -339,6 +349,19 @@ describe('Picker tests', () => {
), { timeout: 5000 })
}, 10000)
test('press enter on an empty search list', async () => {
await tick(120)
type(getByRole('combobox'), 'xxxyyyzzzhahaha')
await waitFor(() => expect(queryAllByRole('option')).toHaveLength(0))
expect(getByRole('combobox').getAttribute('aria-activedescendant')).toBeFalsy()
await tick(120)
fireEvent.keyDown(getByRole('combobox'), { key: 'Enter', code: 'Enter' })
await tick(120)
// should do nothing basically since there's nothing to search for
expect(queryAllByRole('option')).toHaveLength(0)
expect(getByRole('combobox').getAttribute('aria-activedescendant')).toBeFalsy()
}, 10000)
test('press enter to make first search item active - custom emoji', async () => {
picker.customEmoji = [
{

View File

@ -0,0 +1,127 @@
import { createFramework } from '../../../src/picker/components/Picker/framework.js'
describe('framework', () => {
test('patches a node', () => {
const state = { name: 'foo' }
const { html } = createFramework(state)
let node
const render = () => {
node = html`<div>${html`<span>${state.name}</span>`}</div>`
}
render()
expect(node.outerHTML).toBe('<div><span>foo</span></div>')
state.name = 'bar'
render()
expect(node.outerHTML).toBe('<div><span>bar</span></div>')
})
test('replaces one node with a totally different one', () => {
const state = { name: 'foo' }
const { html } = createFramework(state)
let node
const render = () => {
node = html`<div>${
state.name === 'foo' ? html`<span>${state.name}</span>` : html`<button>${state.name}</button>`
}</div>`
}
render()
expect(node.outerHTML).toBe('<div><span>foo</span></div>')
state.name = 'bar'
render()
expect(node.outerHTML).toBe('<div><button>bar</button></div>')
})
test('return the same exact node after a re-render', () => {
const state = { name: 'foo' }
const { html } = createFramework(state)
let node
let cached
const render = () => {
cached = cached ?? html`<span>${state.name}</span>`
node = html`<div>${cached}</div>`
}
render()
expect(node.outerHTML).toBe('<div><span>foo</span></div>')
render()
expect(node.outerHTML).toBe('<div><span>foo</span></div>')
})
test('render two dynamic expressions inside the same element', () => {
const state = { name1: 'foo', name2: 'bar' }
const { html } = createFramework(state)
let node
const render = () => {
node = html`<div>${state.name1}${state.name2}</div>`
}
render()
expect(node.outerHTML).toBe('<div>foobar</div>')
state.name1 = 'baz'
state.name2 = 'quux'
render()
expect(node.outerHTML).toBe('<div>bazquux</div>')
})
test('render a mix of dynamic and static text nodes in the same element', () => {
const state = { name1: 'foo', name2: 'bar' }
const { html } = createFramework(state)
let node
const render = () => {
node = html`<div>1${state.name1}2${state.name2}3</div>`
}
render()
expect(node.outerHTML).toBe('<div>1foo2bar3</div>')
state.name1 = 'baz'
state.name2 = 'quux'
render()
expect(node.outerHTML).toBe('<div>1baz2quux3</div>')
})
test('attributes', () => {
const state = {}
const { html } = createFramework(state)
const expectRender = (render, expected1, expected2) => {
state.name = 'foo'
expect(render().outerHTML).toBe(expected1)
state.name = 'bar'
expect(render().outerHTML).toBe(expected2)
}
expectRender(() => html`<div class="${state.name}"></div>`, '<div class="foo"></div>', '<div class="bar"></div>')
expectRender(() => html`<div class=${state.name}></div>`, '<div class="foo"></div>', '<div class="bar"></div>')
// pre
expectRender(() => html`<div class="a${state.name}"></div>`, '<div class="afoo"></div>', '<div class="abar"></div>')
expectRender(() => html`<div class=a${state.name}></div>`, '<div class="afoo"></div>', '<div class="abar"></div>')
// post
expectRender(() => html`<div class="${state.name}z"></div>`, '<div class="fooz"></div>', '<div class="barz"></div>')
expectRender(() => html`<div class=${state.name}z></div>`, '<div class="fooz"></div>', '<div class="barz"></div>')
// pre+post
expectRender(() => html`<div class="a${state.name}z"></div>`, '<div class="afooz"></div>', '<div class="abarz"></div>')
expectRender(() => html`<div class=a${state.name}z></div>`, '<div class="afooz"></div>', '<div class="abarz"></div>')
})
})

299
yarn.lock
View File

@ -12,7 +12,7 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1":
"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
@ -625,7 +625,7 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
@ -761,14 +761,6 @@
estree-walker "^2.0.2"
magic-string "^0.30.3"
"@rollup/pluginutils@^4.1.0":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^5.0.1":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c"
@ -960,7 +952,15 @@
dependencies:
"@babel/types" "^7.20.7"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.1":
"@types/clean-css@*":
version "4.2.11"
resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.11.tgz#3f170dedd8d096fe7e7bd1c8dda0c8314217cbe6"
integrity sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==
dependencies:
"@types/node" "*"
source-map "^0.6.0"
"@types/estree@*", "@types/estree@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453"
integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==
@ -979,6 +979,15 @@
dependencies:
"@types/node" "*"
"@types/html-minifier@^3.5.3":
version "3.5.3"
resolved "https://registry.yarnpkg.com/@types/html-minifier/-/html-minifier-3.5.3.tgz#5276845138db2cebc54c789e0aaf87621a21e84f"
integrity sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==
dependencies:
"@types/clean-css" "*"
"@types/relateurl" "*"
"@types/uglify-js" "*"
"@types/http-cache-semantics@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz#abe102d06ccda1efdf0ed98c10ccf7f36a785a41"
@ -1047,10 +1056,10 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
"@types/pug@^2.0.6":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.7.tgz#ffb9239e4da7ea1af27070cad9343049e440993d"
integrity sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==
"@types/relateurl@*":
version "0.2.33"
resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.33.tgz#fa174c30100d91e88d7b0ba60cefd7e8c532516f"
integrity sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==
"@types/resolve@1.20.2":
version "1.20.2"
@ -1072,6 +1081,13 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
"@types/uglify-js@*":
version "3.17.4"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.4.tgz#3c70021f08023e5a760ce133d22966f200e1d31c"
integrity sha512-Hm/T0kV3ywpJyMGNbsItdivRhYNCQQf1IIsYsXnoVPES4t+FMLyDe0/K+Ea7ahWtMtSNb22ZdY7MIyoD9rqARg==
dependencies:
source-map "^0.6.1"
"@types/yargs-parser@*":
version "21.0.1"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b"
@ -1130,7 +1146,7 @@ acorn-walk@^8.0.2:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.1.0, acorn@^8.10.0, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
acorn@^8.1.0, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
@ -1268,7 +1284,7 @@ aria-query@5.1.3:
dependencies:
deep-equal "^2.0.5"
aria-query@^5.0.0, aria-query@^5.3.0:
aria-query@^5.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
@ -1429,13 +1445,6 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axobject-query@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
dependencies:
dequal "^2.0.3"
b4a@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9"
@ -1593,7 +1602,7 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-crc32@^0.2.5, buffer-crc32@~0.2.3:
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
@ -1677,6 +1686,14 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camel-case@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
integrity sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==
dependencies:
no-case "^2.2.0"
upper-case "^1.1.1"
camelcase-keys@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
@ -1779,6 +1796,13 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==
clean-css@^4.2.1:
version "4.2.4"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==
dependencies:
source-map "~0.6.0"
cli-cursor@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea"
@ -1827,17 +1851,6 @@ co@^4.6.0:
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
code-red@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35"
integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
"@types/estree" "^1.0.1"
acorn "^8.10.0"
estree-walker "^3.0.3"
periscopic "^3.1.0"
coffee-script@^1.12.4:
version "1.12.7"
resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53"
@ -1914,7 +1927,7 @@ commander@11.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
commander@^2.20.0:
commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -2509,11 +2522,6 @@ destroy@1.2.0, destroy@^1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
detect-indent@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@ -2775,11 +2783,6 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es6-promise@^3.1.2:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -3027,18 +3030,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.1, estree-walker@^2.0.2:
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
estree-walker@^3.0.0, estree-walker@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@ -3734,7 +3730,7 @@ got@^12.1.0:
p-cancelable "^3.0.0"
responselike "^3.0.0"
graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@ -3825,6 +3821,11 @@ has@^1.0.3:
resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6"
integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -3849,6 +3850,19 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-minifier@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56"
integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==
dependencies:
camel-case "^3.0.0"
clean-css "^4.2.1"
commander "^2.19.0"
he "^1.2.0"
param-case "^2.1.1"
relateurl "^0.2.7"
uglify-js "^3.5.1"
html-tags@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
@ -4307,13 +4321,6 @@ is-reference@1.2.1:
dependencies:
"@types/estree" "*"
is-reference@^3.0.0, is-reference@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c"
integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==
dependencies:
"@types/estree" "*"
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@ -5309,11 +5316,6 @@ load-json-file@^5.2.0:
strip-bom "^3.0.0"
type-fest "^0.3.0"
locate-character@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974"
integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -5464,6 +5466,11 @@ loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lower-case@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==
lowercase-keys@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2"
@ -5493,14 +5500,14 @@ lz-string@^1.5.0:
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
magic-string@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
magic-string@^0.25.0:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13"
sourcemap-codec "^1.4.8"
magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5:
magic-string@^0.30.3:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
@ -5694,6 +5701,17 @@ min-indent@^1.0.0, min-indent@^1.0.1:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minify-html-literals@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/minify-html-literals/-/minify-html-literals-1.3.5.tgz#11c05e2b9699be7f41647186a9fe8249b7de6734"
integrity sha512-p8T8ryePRR8FVfJZLVFmM53WY25FL0moCCTycUDuAu6rf9GMLwy0gNjXBGNin3Yun7Y+tIWd28axOf0t2EpAlQ==
dependencies:
"@types/html-minifier" "^3.5.3"
clean-css "^4.2.1"
html-minifier "^4.0.0"
magic-string "^0.25.0"
parse-literals "^1.2.1"
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -5740,13 +5758,6 @@ mkdirp-classic@^0.5.2:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"
modify-values@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
@ -5797,6 +5808,13 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
dependencies:
lower-case "^1.1.1"
node-fetch@^2.6.12:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
@ -6138,6 +6156,13 @@ pako@~1.0.2:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
param-case@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
integrity sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==
dependencies:
no-case "^2.2.0"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -6163,6 +6188,13 @@ parse-json@^5.0.0, parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
parse-literals@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/parse-literals/-/parse-literals-1.2.1.tgz#2311855a12a6e12434f44eb40fa434c48cc0560f"
integrity sha512-Ml0w104Ph2wwzuRdxrg9booVWsngXbB4bZ5T2z6WyF8b5oaNkUmBiDtahi34yUIpXD8Y13JjAK6UyIyApJ73RQ==
dependencies:
typescript "^2.9.2 || ^3.0.0 || ^4.0.0"
parse5@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
@ -6247,21 +6279,12 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
periscopic@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a"
integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^3.0.0"
is-reference "^3.0.0"
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -6776,6 +6799,11 @@ regexpp@^3.0.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
remarkable@^1.7.1:
version "1.7.4"
resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00"
@ -6887,13 +6915,6 @@ rfdc@^1.3.0:
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rimraf@^2.5.2:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -6906,14 +6927,6 @@ rollup-plugin-analyzer@^4.0.0:
resolved "https://registry.yarnpkg.com/rollup-plugin-analyzer/-/rollup-plugin-analyzer-4.0.0.tgz#96b757ed64a098b59d72f085319e68cdd86d5798"
integrity sha512-LL9GEt3bkXp6Wa19SNR5MWcvHNMvuTFYg+eYBZN2OIFhSWN+pEJUQXEKu5BsOeABob3x9PDaLKW7w5iOJnsESQ==
rollup-plugin-svelte@^7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.6.tgz#44a4ea6c6e8ed976824d9fd40c78d048515e5838"
integrity sha512-nVFRBpGWI2qUY1OcSiEEA/kjCY2+vAjO9BI8SzA7NRrh2GTunLd6w2EYmnMt/atgdg8GvcNjLsmZmbQs/u4SQA==
dependencies:
"@rollup/pluginutils" "^4.1.0"
resolve.exports "^2.0.0"
rollup-plugin-terser@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
@ -6985,16 +6998,6 @@ safe-regex-test@^1.0.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sander@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad"
integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==
dependencies:
es6-promise "^3.1.2"
graceful-fs "^4.1.3"
mkdirp "^0.5.1"
rimraf "^2.5.2"
sanitize-filename@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
@ -7233,16 +7236,6 @@ socks@^2.7.1:
ip "^2.0.0"
smart-buffer "^4.2.0"
sorcery@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.0.tgz#310c80ee993433854bb55bb9aa4003acd147fca8"
integrity sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.14"
buffer-crc32 "^0.2.5"
minimist "^1.2.0"
sander "^0.5.0"
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@ -7264,11 +7257,16 @@ source-map-support@^0.5.16, source-map-support@~0.5.20:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@ -7652,41 +7650,6 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svelte-jester@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/svelte-jester/-/svelte-jester-3.0.0.tgz#7872beb559bce3c66f134d6f016f2626cf8f1d1c"
integrity sha512-V279cL906++hn00hkL1xAr/y5OjjxPYWic1g0yTJFmqdbdWKthdcuP3XBvmmwP9AzFBT51DlPgXz56HItle1Ug==
svelte-preprocess@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.1.tgz#53d7107c2e8b307afd4418e06239177c4de12025"
integrity sha512-p/Dp4hmrBW5mrCCq29lEMFpIJT2FZsRlouxEc5qpbOmXRbaFs7clLs8oKPwD3xCFyZfv1bIhvOzpQkhMEVQdMw==
dependencies:
"@types/pug" "^2.0.6"
detect-indent "^6.1.0"
magic-string "^0.27.0"
sorcery "^0.11.0"
strip-indent "^3.0.0"
svelte@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.8.tgz#a279d8b6646131ffb11bc692840f8839b8ae4ed1"
integrity sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==
dependencies:
"@ampproject/remapping" "^2.2.1"
"@jridgewell/sourcemap-codec" "^1.4.15"
"@jridgewell/trace-mapping" "^0.3.18"
acorn "^8.9.0"
aria-query "^5.3.0"
axobject-query "^3.2.1"
code-red "^1.0.3"
css-tree "^2.3.1"
estree-walker "^3.0.3"
is-reference "^3.0.1"
locate-character "^3.0.0"
magic-string "^0.30.4"
periscopic "^3.1.0"
svg-tags@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
@ -8070,6 +8033,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
"typescript@^2.9.2 || ^3.0.0 || ^4.0.0":
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typical@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
@ -8085,7 +8053,7 @@ ua-parser-js@^1.0.2:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c"
integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==
uglify-js@^3.1.4:
uglify-js@^3.1.4, uglify-js@^3.5.1:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==
@ -8136,6 +8104,11 @@ update-browserslist-db@^1.0.13:
escalade "^3.1.1"
picocolors "^1.0.0"
upper-case@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
integrity sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"