0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-02-26 23:54:23 +01:00

Be SSR friendly when accessing DOM objects

This commit is contained in:
Johann-S 2021-02-17 09:22:44 +02:00 committed by XhmikosR
parent c44d99f55c
commit 8306213650
23 changed files with 203 additions and 70 deletions

View File

@ -1,8 +1,10 @@
{
"root": true,
"plugins": ["ssr-friendly"],
"extends": [
"plugin:import/errors",
"plugin:import/warnings",
"plugin:ssr-friendly/recommended",
"plugin:unicorn/recommended",
"xo",
"xo/browser"
@ -50,6 +52,8 @@
"error",
"never"
],
"ssr-friendly/no-dom-globals-in-react-cc-render": "off",
"ssr-friendly/no-dom-globals-in-react-fc": "off",
"unicorn/explicit-length-check": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-method-this-argument": "off",

View File

@ -6,7 +6,12 @@
*/
import Data from './dom/data'
import { executeAfterTransition, getElement } from './util/index'
import {
executeAfterTransition,
getElement,
getWindow,
getDocument
} from './util/index'
import EventHandler from './dom/event-handler'
import Config from './util/config'
@ -30,6 +35,8 @@ class BaseComponent extends Config {
}
this._element = element
this._window = getWindow()
this._document = getDocument()
this._config = this._getConfig(config)
Data.set(this._element, this.constructor.DATA_KEY, this)

View File

@ -5,7 +5,7 @@
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin } from './util/index'
import { defineJQueryPlugin, getDocument } from './util/index'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
@ -54,7 +54,7 @@ class Button extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
event.preventDefault()
const button = event.target.closest(SELECTOR_DATA_TOGGLE)

View File

@ -12,7 +12,9 @@ import {
isRTL,
isVisible,
reflow,
triggerTransitionEnd
triggerTransitionEnd,
getDocument,
getWindow
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@ -129,7 +131,7 @@ class Carousel extends BaseComponent {
// FIXME TODO use `document.visibilityState`
// Don't call next when the page isn't visible
// or the carousel or its parent isn't visible
if (!document.hidden && isVisible(this._element)) {
if (!this._document.hidden && isVisible(this._element)) {
this.next()
}
}
@ -505,9 +507,9 @@ class Carousel extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler)
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler)
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (const carousel of carousels) {

View File

@ -10,7 +10,8 @@ import {
getElement,
getElementFromSelector,
getSelectorFromElement,
reflow
reflow,
getDocument
} from './util/index'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
@ -279,7 +280,7 @@ class Collapse extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
// preventDefault only for <a> elements (which change the URL) not inside the collapsible element
if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {
event.preventDefault()

View File

@ -5,6 +5,8 @@
* --------------------------------------------------------------------------
*/
import { getWindow } from '../util/index'
function normalizeData(value) {
if (value === 'true') {
return true
@ -61,10 +63,11 @@ const Manipulator = {
offset(element) {
const rect = element.getBoundingClientRect()
const windowRef = getWindow()
return {
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset
top: rect.top + windowRef.pageYOffset,
left: rect.left + windowRef.pageXOffset
}
},

View File

@ -5,18 +5,18 @@
* --------------------------------------------------------------------------
*/
import { isDisabled, isVisible } from '../util/index'
import { getDocument, isDisabled, isVisible } from '../util/index'
/**
* Constants
*/
const SelectorEngine = {
find(selector, element = document.documentElement) {
find(selector, element = getDocument().documentElement) {
return [].concat(...Element.prototype.querySelectorAll.call(element, selector))
},
findOne(selector, element = document.documentElement) {
findOne(selector, element = getDocument().documentElement) {
return Element.prototype.querySelector.call(element, selector)
},

View File

@ -14,7 +14,8 @@ import {
isElement,
isRTL,
isVisible,
noop
noop,
getDocument
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@ -133,8 +134,8 @@ class Dropdown extends BaseComponent {
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...document.body.children)) {
if ('ontouchstart' in this._document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...this._document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
@ -434,11 +435,13 @@ class Dropdown extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const documentRef = getDocument()
EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
EventHandler.on(documentRef, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
EventHandler.on(documentRef, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
EventHandler.on(documentRef, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
Dropdown.getOrCreateInstance(this).toggle()
})

View File

@ -5,7 +5,14 @@
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index'
import {
defineJQueryPlugin,
getElementFromSelector,
isRTL,
isVisible,
reflow,
getDocument
} from './util/index'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import ScrollBarHelper from './util/scrollbar'
@ -184,8 +191,8 @@ class Modal extends BaseComponent {
_showElement(relatedTarget) {
// try to append dynamic modal
if (!document.body.contains(this._element)) {
document.body.append(this._element)
if (!this._document.body.contains(this._element)) {
this._document.body.append(this._element)
}
this._element.style.display = 'block'
@ -255,7 +262,7 @@ class Modal extends BaseComponent {
this._isTransitioning = false
this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN)
this._document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments()
this._scrollBar.reset()
EventHandler.trigger(this._element, EVENT_HIDDEN)
@ -272,7 +279,7 @@ class Modal extends BaseComponent {
return
}
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight
const initialOverflowY = this._element.style.overflowY
// return if the following background transition hasn't yet completed
if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
@ -299,7 +306,7 @@ class Modal extends BaseComponent {
*/
_adjustDialog() {
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight
const scrollbarWidth = this._scrollBar.getWidth()
const isBodyOverflowing = scrollbarWidth > 0
@ -341,7 +348,7 @@ class Modal extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) {

View File

@ -9,7 +9,9 @@ import {
defineJQueryPlugin,
getElementFromSelector,
isDisabled,
isVisible
isVisible,
getDocument,
getWindow
} from './util/index'
import ScrollBarHelper from './util/scrollbar'
import EventHandler from './dom/event-handler'
@ -209,7 +211,7 @@ class Offcanvas extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) {
@ -237,7 +239,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
data.toggle(this)
})
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => {
for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {
Offcanvas.getOrCreateInstance(selector).show()
}

View File

@ -5,7 +5,12 @@
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin, getElement, getSelectorFromElement } from './util/index'
import {
defineJQueryPlugin,
getElement,
getSelectorFromElement,
getWindow
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
@ -58,7 +63,7 @@ const DefaultType = {
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element, config)
this._scrollElement = this._element.tagName === 'BODY' ? window : this._element
this._scrollElement = this._element.tagName === 'BODY' ? this._window : this._element
this._offsets = []
this._targets = []
this._activeTarget = null
@ -137,14 +142,14 @@ class ScrollSpy extends BaseComponent {
_getScrollHeight() {
return this._scrollElement.scrollHeight || Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
this._document.body.scrollHeight,
this._document.documentElement.scrollHeight
)
}
_getOffsetHeight() {
return this._scrollElement === window ?
window.innerHeight :
this._window.innerHeight :
this._scrollElement.getBoundingClientRect().height
}
@ -251,7 +256,7 @@ class ScrollSpy extends BaseComponent {
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => {
for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
new ScrollSpy(spy) // eslint-disable-line no-new
}

View File

@ -5,7 +5,13 @@
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin, getElementFromSelector, isDisabled, reflow } from './util/index'
import {
defineJQueryPlugin,
getDocument,
getElementFromSelector,
isDisabled,
reflow
} from './util/index'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
@ -177,7 +183,7 @@ class Tab extends BaseComponent {
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}

View File

@ -245,8 +245,8 @@ class Tooltip extends BaseComponent {
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement) {
for (const element of [].concat(...document.body.children)) {
if ('ontouchstart' in this._document.documentElement) {
for (const element of [].concat(...this._document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
@ -280,8 +280,8 @@ class Tooltip extends BaseComponent {
// If this is a touch-enabled device we remove the extra
// empty mouseover listeners we added for iOS support
if ('ontouchstart' in document.documentElement) {
for (const element of [].concat(...document.body.children)) {
if ('ontouchstart' in this._document.documentElement) {
for (const element of [].concat(...this._document.body.children)) {
EventHandler.off(element, 'mouseover', noop)
}
}

View File

@ -6,13 +6,13 @@
*/
import EventHandler from '../dom/event-handler'
import { getElementFromSelector, isDisabled } from './index'
import { getElementFromSelector, isDisabled, getDocument } from './index'
const enableDismissTrigger = (component, method = 'hide') => {
const clickEvent = `click.dismiss${component.EVENT_KEY}`
const name = component.NAME
EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) {
EventHandler.on(getDocument(), clickEvent, `[data-bs-dismiss="${name}"]`, function (event) {
if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}

View File

@ -7,6 +7,7 @@
import EventHandler from '../dom/event-handler'
import SelectorEngine from '../dom/selector-engine'
import { getDocument } from './index'
import Config from './config'
/**
@ -43,6 +44,7 @@ class FocusTrap extends Config {
this._config = this._getConfig(config)
this._isActive = false
this._lastTabNavDirection = null
this._document = getDocument()
}
// Getters
@ -68,9 +70,9 @@ class FocusTrap extends Config {
this._config.trapElement.focus()
}
EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop
EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))
EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
EventHandler.off(this._document, EVENT_KEY) // guard against infinite focus loop
EventHandler.on(this._document, EVENT_FOCUSIN, event => this._handleFocusin(event))
EventHandler.on(this._document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
this._isActive = true
}
@ -81,14 +83,14 @@ class FocusTrap extends Config {
}
this._isActive = false
EventHandler.off(document, EVENT_KEY)
EventHandler.off(this._document, EVENT_KEY)
}
// Private
_handleFocusin(event) {
const { trapElement } = this._config
if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {
if (event.target === this._document || event.target === trapElement || trapElement.contains(event.target)) {
return
}

View File

@ -25,7 +25,7 @@ const toType = object => {
const getUID = prefix => {
do {
prefix += Math.floor(Math.random() * MAX_UID)
} while (document.getElementById(prefix))
} while (getDocument().getElementById(prefix))
return prefix
}
@ -59,7 +59,7 @@ const getSelectorFromElement = element => {
const selector = getSelector(element)
if (selector) {
return document.querySelector(selector) ? selector : null
return getDocument().querySelector(selector) ? selector : null
}
return null
@ -68,7 +68,7 @@ const getSelectorFromElement = element => {
const getElementFromSelector = element => {
const selector = getSelector(element)
return selector ? document.querySelector(selector) : null
return selector ? getDocument().querySelector(selector) : null
}
const getTransitionDurationFromElement = element => {
@ -77,7 +77,7 @@ const getTransitionDurationFromElement = element => {
}
// Get transition-duration of the element
let { transitionDuration, transitionDelay } = window.getComputedStyle(element)
let { transitionDuration, transitionDelay } = getWindow().getComputedStyle(element)
const floatTransitionDuration = Number.parseFloat(transitionDuration)
const floatTransitionDelay = Number.parseFloat(transitionDelay)
@ -167,7 +167,7 @@ const isDisabled = element => {
}
const findShadowRoot = element => {
if (!document.documentElement.attachShadow) {
if (!getDocument().documentElement.attachShadow) {
return null
}
@ -200,11 +200,12 @@ const noop = () => {}
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = element => {
element.offsetHeight // eslint-disable-line no-unused-expressions
// eslint-disable-next-line no-unused-expressions
element.offsetHeight
}
const getjQuery = () => {
if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
if (getWindow().jQuery && !getDocument().body.hasAttribute('data-bs-no-jquery')) {
return window.jQuery
}
@ -214,10 +215,11 @@ const getjQuery = () => {
const DOMContentLoadedCallbacks = []
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
const documentRef = getDocument()
if (documentRef.readyState === 'loading') {
// add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => {
documentRef.addEventListener('DOMContentLoaded', () => {
for (const callback of DOMContentLoadedCallbacks) {
callback()
}
@ -230,7 +232,7 @@ const onDOMContentLoaded = callback => {
}
}
const isRTL = () => document.documentElement.dir === 'rtl'
const isRTL = () => getDocument().documentElement.dir === 'rtl'
const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
@ -312,11 +314,26 @@ const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed
return list[Math.max(0, Math.min(index, listLength - 1))]
}
/**
* @return {window|{}} The proper element
*/
const getWindow = () => {
return typeof window !== 'undefined' ? window : {}
}
/**
* @return {document|{}} The proper element
*/
const getDocument = () => {
return typeof document !== 'undefined' ? document : {}
}
export {
defineJQueryPlugin,
execute,
executeAfterTransition,
findShadowRoot,
getDocument,
getElement,
getElementFromSelector,
getjQuery,
@ -324,6 +341,7 @@ export {
getSelectorFromElement,
getTransitionDurationFromElement,
getUID,
getWindow,
isDisabled,
isElement,
isRTL,

View File

@ -5,6 +5,8 @@
* --------------------------------------------------------------------------
*/
import { getWindow } from './index'
const uriAttributes = new Set([
'background',
'cite',
@ -91,7 +93,8 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
return sanitizeFunction(unsafeHtml)
}
const domParser = new window.DOMParser()
const windowRef = getWindow()
const domParser = new windowRef.DOMParser()
const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
const elements = [].concat(...createdDocument.body.querySelectorAll('*'))

View File

@ -7,7 +7,7 @@
import SelectorEngine from '../dom/selector-engine'
import Manipulator from '../dom/manipulator'
import { isElement } from './index'
import { isElement, getDocument, getWindow } from './index'
/**
* Constants
@ -24,14 +24,15 @@ const PROPERTY_MARGIN = 'margin-right'
class ScrollBarHelper {
constructor() {
this._element = document.body
this._element = getDocument().body
this._window = getWindow()
}
// Public
getWidth() {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = document.documentElement.clientWidth
return Math.abs(window.innerWidth - documentWidth)
return Math.abs(this._window.innerWidth - documentWidth)
}
hide() {
@ -64,12 +65,12 @@ class ScrollBarHelper {
_setElementAttributes(selector, styleProperty, callback) {
const scrollbarWidth = this.getWidth()
const manipulationCallBack = element => {
if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {
if (element !== this._element && this._window.innerWidth > element.clientWidth + scrollbarWidth) {
return
}
this._saveInitialAttribute(element, styleProperty)
const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)
const calculatedValue = this._window.getComputedStyle(element).getPropertyValue(styleProperty)
element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)
}

View File

@ -7,7 +7,7 @@
import Config from './config'
import EventHandler from '../dom/event-handler'
import { execute } from './index'
import { execute, getDocument, getWindow } from './index'
/**
* Constants
@ -52,7 +52,7 @@ class Swipe extends Config {
this._config = this._getConfig(config)
this._deltaX = 0
this._supportPointerEvents = Boolean(window.PointerEvent)
this._supportPointerEvents = Boolean(getWindow().PointerEvent)
this._initEvents()
}
@ -139,7 +139,7 @@ class Swipe extends Config {
// Static
static isSupported() {
return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
return 'ontouchstart' in getDocument().documentElement || navigator.maxTouchPoints > 0
}
}

View File

@ -1,6 +1,7 @@
import Tooltip from '../../dist/tooltip'
import '../../dist/carousel'
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode))

View File

@ -1,5 +1,6 @@
import { Tooltip } from '../../../dist/js/bootstrap.esm.js'
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode))

66
package-lock.json generated
View File

@ -24,6 +24,7 @@
"eslint": "^8.8.0",
"eslint-config-xo": "^0.39.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-ssr-friendly": "^1.0.5",
"eslint-plugin-unicorn": "^40.1.0",
"find-unused-sass-variables": "^3.1.0",
"globby": "^11.0.4",
@ -4319,6 +4320,45 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"node_modules/eslint-plugin-ssr-friendly": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-ssr-friendly/-/eslint-plugin-ssr-friendly-1.0.5.tgz",
"integrity": "sha512-F1vKfzhOnrIXhcx91Y3r1x8vjJAoCex25PUgYErOe6q95T4KuCTz6+LgGQ4TTvhBdCfNqu1U0krAHe3UNuEOqg==",
"dev": true,
"dependencies": {
"globals": "^13.2.0"
},
"peerDependencies": {
"eslint": ">=0.8.0"
}
},
"node_modules/eslint-plugin-ssr-friendly/node_modules/globals": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
"integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-ssr-friendly/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "40.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz",
@ -14030,6 +14070,32 @@
}
}
},
"eslint-plugin-ssr-friendly": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-ssr-friendly/-/eslint-plugin-ssr-friendly-1.0.5.tgz",
"integrity": "sha512-F1vKfzhOnrIXhcx91Y3r1x8vjJAoCex25PUgYErOe6q95T4KuCTz6+LgGQ4TTvhBdCfNqu1U0krAHe3UNuEOqg==",
"dev": true,
"requires": {
"globals": "^13.2.0"
},
"dependencies": {
"globals": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
"integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
}
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
}
}
},
"eslint-plugin-unicorn": {
"version": "40.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz",

View File

@ -113,6 +113,7 @@
"eslint": "^8.8.0",
"eslint-config-xo": "^0.39.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-ssr-friendly": "^1.0.5",
"eslint-plugin-unicorn": "^40.1.0",
"find-unused-sass-variables": "^3.1.0",
"globby": "^11.0.4",