diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index bda7143fc..a19d33375 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -295,12 +295,6 @@ const BrewRenderer = (props)=>{ rowGap : `${displayOptions.rowGap}px` }; - const styleObject = {}; - - if(global.config.deployment) { - styleObject.backgroundImage = `url("data:image/svg+xml;utf8,${global.config.deployment}")`; - } - const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]); @@ -329,10 +323,9 @@ const BrewRenderer = (props)=>{ contentDidMount={frameDidMount} onClick={()=>{emitClick();}} > -
{/* Apply CSS from Style tab and render pages from Markdown tab */} diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index b0a3e9779..bb4fe69c5 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -6,7 +6,6 @@ overflow-y : scroll; will-change : transform; &:has(.facing, .flow) { padding : 60px 30px; } - &.deployment { background-color : darkred; } :where(.pages) { &.facing { display : grid; diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index a067ac4af..8b7f6ccf5 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -325,10 +325,10 @@ const Editor = createClass({ const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const currentPos = brewRenderer.scrollTop; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; - - const checkIfScrollComplete = ()=>{ - let scrollingTimeout; - clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs + + let scrollingTimeout; + const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times + clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs scrollingTimeout = setTimeout(()=>{ isJumping = false; brewRenderer.removeEventListener('scroll', checkIfScrollComplete); @@ -369,8 +369,8 @@ const Editor = createClass({ let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); - const checkIfScrollComplete = ()=>{ - let scrollingTimeout; + let scrollingTimeout; + const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs scrollingTimeout = setTimeout(()=>{ isJumping = false; diff --git a/client/homebrew/editor/metadataEditor/validations.js b/client/homebrew/editor/metadataEditor/validations.js index 858fca6c4..d0e052b07 100644 --- a/client/homebrew/editor/metadataEditor/validations.js +++ b/client/homebrew/editor/metadataEditor/validations.js @@ -18,7 +18,7 @@ module.exports = { try { Boolean(new URL(value)); return null; - } catch (e) { + } catch { return 'Must be a valid URL'; } } diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index d53d3770e..e38293bde 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -19,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{ const params = useParams(); const [searchParams] = useSearchParams(); const queryParams = Object.fromEntries(searchParams?.entries() || []); - return ; }; @@ -46,11 +45,20 @@ const Homebrew = (props)=>{ global.version = version; global.config = config; + const backgroundObject = ()=>{ + if(global.config.deployment || (config.local && config.development)){ + const bgText = global.config.deployment || 'Local'; + return { + backgroundImage : `url("data:image/svg+xml;utf8,${bgText}")` + }; + } + return null; + }; updateLocalStorage(); return ( -
+
} /> } /> diff --git a/client/homebrew/homebrew.less b/client/homebrew/homebrew.less index e265c2941..2cbc35857 100644 --- a/client/homebrew/homebrew.less +++ b/client/homebrew/homebrew.less @@ -1,12 +1,14 @@ @import 'naturalcrit/styles/core.less'; .homebrew { height : 100%; + background-color:@steel; + &.deployment { background-color : darkred; } + .sitePage { display : flex; flex-direction : column; height : 100%; overflow-y : hidden; - background-color : @steel; .content { position : relative; flex : auto; diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index 6d9bec444..a2650dbc8 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -2,9 +2,9 @@ require('./error-navitem.less'); const React = require('react'); const Nav = require('naturalcrit/nav/nav.jsx'); -const ErrorNavItem = ({error = '', clearError})=>{ +const ErrorNavItem = ({ error = '', clearError })=>{ const response = error.response; - const errorCode = error.code + const errorCode = error.code; const status = response?.status; const HBErrorCode = response?.body?.HBErrorCode; const message = response?.body?.message; @@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{ errMsg += `\`\`\`\n${error.stack}\n`; errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; console.log(errMsg); - } catch (e){} + } catch {} if(status === 409) { return @@ -112,6 +112,15 @@ const ErrorNavItem = ({error = '', clearError})=>{ ; } + if(HBErrorCode === '13') { + return + Oops! +
+ Server has lost connection to the database. +
+
; + } + if(errorCode === 'ECONNABORTED') { return Oops! diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx index ccade4e8b..d19b7595f 100644 --- a/client/homebrew/navbar/newbrew.navitem.jsx +++ b/client/homebrew/navbar/newbrew.navitem.jsx @@ -34,8 +34,8 @@ const NewBrew = ()=>{ } const type = file.name.split('.').pop().toLowerCase(); - - alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); + + alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); console.log(file); diff --git a/client/homebrew/navbar/share.navitem.jsx b/client/homebrew/navbar/share.navitem.jsx index a08ac6878..0cd1a52b7 100644 --- a/client/homebrew/navbar/share.navitem.jsx +++ b/client/homebrew/navbar/share.navitem.jsx @@ -2,22 +2,22 @@ import React from 'react'; import dedent from 'dedent-tabs'; import Nav from 'naturalcrit/nav/nav.jsx'; - const getShareId = (brew)=>( - brew.googleId && !brew.stubbed - ? brew.googleId + brew.shareId - : brew.shareId - ); +const getShareId = (brew)=>( + brew.googleId && !brew.stubbed + ? brew.googleId + brew.shareId + : brew.shareId +); - const getRedditLink = (brew)=>{ - const text = dedent` +const getRedditLink = (brew)=>{ + const text = dedent` Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. **[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`; - return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }; + return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`; +}; -export default ({brew}) => ( +export default ({ brew })=>( share diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index e3d449a55..524f31ac9 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,6 +5,7 @@ import './editPage.less'; import React, { useState, useEffect, useRef } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; +import _ from 'lodash'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; @@ -25,7 +26,6 @@ import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; // Page specific imports import { Meta } from 'vitreum/headtags'; -import _ from 'lodash'; import { md5 } from 'hash-wasm'; import { gzipSync, strToU8 } from 'fflate'; import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; @@ -46,8 +46,8 @@ const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; const METAKEY = 'HB_newPage_meta'; - const useLocalStorage = false; +const neverSaved = false; const EditPage = (props)=>{ props = { @@ -123,16 +123,16 @@ const EditPage = (props)=>{ editorRef.current?.update(); }; - const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' - if (subfield == 'renderer' || subfield == 'theme') + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' + if(subfield == 'renderer' || subfield == 'theme') fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length && (field == 'text' || field == 'snippets')) setHTMLErrors(Markdown.validate(value)); - if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); - else setCurrentBrew(prev => ({ ...prev, [field]: value })); + if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); + else setCurrentBrew((prev)=>({ ...prev, [field]: value })); if(useLocalStorage) { if(field == 'text') localStorage.setItem(BREWKEY, value); @@ -309,14 +309,18 @@ const EditPage = (props)=>{ // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) - return trySave(true)} color='blue' icon='fas fa-save'>Save Now; + return trySave(true)} color='blue' icon='fas fa-save'>save now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) - return auto-saved.; + return auto-saved; + + // #5 - No unsaved changes, and has never been saved, hide the button + if(neverSaved) + return save now; // DEFAULT - No unsaved changes, show SAVED - return saved.; + return saved; }; const toggleAutoSave = ()=>{ diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index c0220b648..89abd570f 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -196,6 +196,12 @@ const errorIndex = (props)=>{ **Brew ID:** ${props.brew.brewId}`, + // Database Connection Lost + '13' : dedent` + ## Database connection has been lost. + + The server could not communicate with the database.`, + //account page when account is not defined '50' : dedent` ## You are not signed in diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index fdc439ab5..82c5b7084 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -5,6 +5,7 @@ import './homePage.less'; import React, { useState, useEffect, useRef } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; +import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; @@ -32,25 +33,28 @@ const SNIPKEY = 'homebrewery-new-snippets'; const METAKEY = 'homebrewery-new-meta'; const useLocalStorage = false; +const neverSaved = true; const HomePage =(props)=>{ props = { brew : DEFAULT_BREW, ver : '0.0.0', - ...props - }; + ...props + }; const [currentBrew , setCurrentBrew] = useState(props.brew); - const [welcomeText , setWelcomeText] = useState(props.brew.text); const [error , setError] = useState(undefined); const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text)); const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle , setThemeBundle] = useState({}); + const [unsavedChanges , setUnsavedChanges] = useState(false); const [isSaving , setIsSaving] = useState(false); + const [autoSaveEnabled , setAutoSaveEnable] = useState(false); - const editorRef = useRef(null); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); useEffect(()=>{ fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -67,7 +71,7 @@ const HomePage =(props)=>{ document.addEventListener('keydown', handleControlKeys); - return () => { + return ()=>{ document.removeEventListener('keydown', handleControlKeys); }; }, []); @@ -85,20 +89,27 @@ const HomePage =(props)=>{ }); }; + useEffect(()=>{ + const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); + setUnsavedChanges(hasChange); + + if(autoSaveEnabled) trySave(false, hasChange); + }, [currentBrew]); + const handleSplitMove = ()=>{ editorRef.current.update(); }; - const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' - if (subfield == 'renderer' || subfield == 'theme') + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' + if(subfield == 'renderer' || subfield == 'theme') fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length && (field == 'text' || field == 'snippets')) setHTMLErrors(Markdown.validate(value)); - if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); - else setCurrentBrew(prev => ({ ...prev, [field]: value })); + if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); + else setCurrentBrew((prev)=>({ ...prev, [field]: value })); if(useLocalStorage) { if(field == 'text') localStorage.setItem(BREWKEY, value); @@ -112,6 +123,41 @@ const HomePage =(props)=>{ } }; + const renderSaveButton = ()=>{ + // #1 - Currently saving, show SAVING + if(isSaving) + return saving...; + + // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING + // if(unsavedChanges && warnUnsavedChanges) { + // resetWarnUnsavedTimer(); + // const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + // const text = elapsedTime === 0 + // ? 'Autosave is OFF.' + // : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + + // return + // Reminder... + //
{text}
+ //
; + // } + + // #3 - Unsaved changes exist, click to save, show SAVE NOW + if(unsavedChanges) + return save now; + + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED + if(autoSaveEnabled) + return auto-saved; + + // #5 - No unsaved changes, and has never been saved, hide the button + if(neverSaved) + return save now; + + // DEFAULT - No unsaved changes, show SAVED + return saved; + }; + const clearError = ()=>{ setError(null); setIsSaving(false); @@ -120,10 +166,9 @@ const HomePage =(props)=>{ const renderNavbar = ()=>{ return - {error ? - : - null - } + {error + ? + : renderSaveButton()} @@ -165,7 +210,7 @@ const HomePage =(props)=>{ />
-
+
Save current
@@ -173,7 +218,7 @@ const HomePage =(props)=>{ Create your own
- ) + ); }; module.exports = HomePage; diff --git a/client/homebrew/pages/homePage/homePage.less b/client/homebrew/pages/homePage/homePage.less index 4cf9ff4fe..c3ec1815c 100644 --- a/client/homebrew/pages/homePage/homePage.less +++ b/client/homebrew/pages/homePage/homePage.less @@ -34,7 +34,13 @@ } .navItem.save { + .fadeInRight(); + .transition(opacity); background-color : @orange; &:hover { background-color : @green; } + &.neverSaved { + .fadeOutRight(); + opacity: 0; + } } } diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index d1d1fa75c..dccf7deb7 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -5,6 +5,7 @@ import './newPage.less'; import React, { useState, useEffect, useRef } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; +import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; @@ -26,19 +27,18 @@ import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; // Page specific imports import { Meta } from 'vitreum/headtags'; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const METAKEY = 'HB_newPage_metadata'; const SNIPKEY = 'HB_newPage_snippets'; const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; - const useLocalStorage = true; +const neverSaved = true; -const NewPage = (props) => { +const NewPage = (props)=>{ props = { - brew: DEFAULT_BREW, + brew : DEFAULT_BREW, ...props }; @@ -51,10 +51,13 @@ const NewPage = (props) => { const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle , setThemeBundle ] = useState({}); + const [unsavedChanges , setUnsavedChanges ] = useState(false); + const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false); - const editorRef = useRef(null); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - useEffect(() => { + useEffect(()=>{ loadBrew(); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -70,7 +73,7 @@ const NewPage = (props) => { document.addEventListener('keydown', handleControlKeys); - return () => { + return ()=>{ document.removeEventListener('keydown', handleControlKeys); }; }, []); @@ -93,6 +96,7 @@ const NewPage = (props) => { const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; setCurrentBrew(brew); + lastSavedBrew.current = brew; setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); localStorage.setItem(BREWKEY, brew.text); @@ -103,20 +107,27 @@ const NewPage = (props) => { window.history.replaceState({}, window.location.title, '/new/'); }; + useEffect(()=>{ + const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); + setUnsavedChanges(hasChange); + + if(autoSaveEnabled) trySave(false, hasChange); + }, [currentBrew]); + const handleSplitMove = ()=>{ editorRef.current.update(); }; - const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' - if (subfield == 'renderer' || subfield == 'theme') + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' + if(subfield == 'renderer' || subfield == 'theme') fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length && (field == 'text' || field == 'snippets')) setHTMLErrors(Markdown.validate(value)); - if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); - else setCurrentBrew(prev => ({ ...prev, [field]: value })); + if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); + else setCurrentBrew((prev)=>({ ...prev, [field]: value })); if(useLocalStorage) { if(field == 'text') localStorage.setItem(BREWKEY, value); @@ -130,10 +141,10 @@ const NewPage = (props) => { } }; - const save = async () => { + const save = async ()=>{ setIsSaving(true); - let updatedBrew = { ...currentBrew }; + const updatedBrew = { ...currentBrew }; splitTextStyleAndMetadata(updatedBrew); const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; @@ -142,13 +153,13 @@ const NewPage = (props) => { const res = await request .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) .send(updatedBrew) - .catch((err) => { + .catch((err)=>{ setIsSaving(false); setError(err); }); - setIsSaving(false) - if (!res) return; + setIsSaving(false); + if(!res) return; const savedBrew = res.body; @@ -159,15 +170,38 @@ const NewPage = (props) => { }; const renderSaveButton = ()=>{ - if(isSaving){ - return - save... - ; - } else { - return - save - ; - } + // #1 - Currently saving, show SAVING + if(isSaving) + return saving...; + + // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING + // if(unsavedChanges && warnUnsavedChanges) { + // resetWarnUnsavedTimer(); + // const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + // const text = elapsedTime === 0 + // ? 'Autosave is OFF.' + // : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + + // return + // Reminder... + //
{text}
+ //
; + // } + + // #3 - Unsaved changes exist, click to save, show SAVE NOW + if(unsavedChanges) + return save now; + + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED + if(autoSaveEnabled) + return auto-saved; + + // #5 - No unsaved changes, and has never been saved, hide the button + if(neverSaved) + return save now; + + // DEFAULT - No unsaved changes, show SAVED + return saved; }; const clearError = ()=>{ @@ -175,7 +209,7 @@ const NewPage = (props) => { setIsSaving(false); }; - const renderNavbar = () => ( + const renderNavbar = ()=>( {currentBrew.title} @@ -196,7 +230,7 @@ const NewPage = (props) => { ); return ( -
+
{renderNavbar()}
diff --git a/client/homebrew/pages/newPage/newPage.less b/client/homebrew/pages/newPage/newPage.less index ebc44d543..083e1ee09 100644 --- a/client/homebrew/pages/newPage/newPage.less +++ b/client/homebrew/pages/newPage/newPage.less @@ -1,6 +1,12 @@ .newPage { .navItem.save { + .fadeInRight(); + .transition(opacity); background-color : @orange; &:hover { background-color : @green; } + &.neverSaved { + .fadeOutRight(); + opacity: 0; + } } } diff --git a/client/homebrew/pages/vaultPage/vaultPage.less b/client/homebrew/pages/vaultPage/vaultPage.less index 8a5f3a714..304fefc72 100644 --- a/client/homebrew/pages/vaultPage/vaultPage.less +++ b/client/homebrew/pages/vaultPage/vaultPage.less @@ -1,14 +1,16 @@ .vaultPage { height : 100%; overflow-y : hidden; - background-color : #2C3E50; *:not(input) { user-select : none; } + .form { + background:white; + } + :where(.content .dataGroup) { width : 100%; height : 100%; - background : white; &.form .brewLookup { position : relative; @@ -171,7 +173,6 @@ max-height : 100%; padding : 70px 50px; overflow-y : scroll; - background-color : #2C3E50; container-type : inline-size; h3 { font-size : 25px; } diff --git a/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.js b/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.js index b4a05974f..f6e01c54f 100644 --- a/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.js +++ b/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.js @@ -26,7 +26,7 @@ const getLocalStorageMap = function(){ if(global?.account?.username){ const username = global.account.username; - localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`; + localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`; } return localStorageMap; diff --git a/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.spec.js b/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.spec.js deleted file mode 100644 index ac61d4add..000000000 --- a/client/homebrew/utils/updateLocalStorage/localStorageKeyMap.spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import getLocalStorageMap from './localStorageKeyMap.js'; - -describe('getLocalStorageMap', ()=>{ - it('no username', ()=>{ - const account = global.account; - - delete global.account; - - const map = getLocalStorageMap(); - - global.account = account; - - expect(map).toBeInstanceOf(Object); - expect(Object.entries(map)).toHaveLength(16); - }); - - it('no username', ()=>{ - const account = global.account; - - global.account = { username: 'test' }; - - const map = getLocalStorageMap(); - - global.account = account; - - expect(map).toBeInstanceOf(Object); - expect(Object.entries(map)).toHaveLength(17); - expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test'); - }); -}); \ No newline at end of file diff --git a/client/homebrew/utils/updateLocalStorage/updateLocalStorageKeys.js b/client/homebrew/utils/updateLocalStorage/updateLocalStorageKeys.js index 1a8231f73..912c4297b 100644 --- a/client/homebrew/utils/updateLocalStorage/updateLocalStorageKeys.js +++ b/client/homebrew/utils/updateLocalStorage/updateLocalStorageKeys.js @@ -4,10 +4,7 @@ const updateLocalStorage = function(){ // Return if no window and thus no local storage if(typeof window === 'undefined') return; - // Return if the local storage key map has no content const localStorageKeyMap = getLocalStorageMap(); - if(Object.keys(localStorageKeyMap).length == 0) return; - const storage = window.localStorage; Object.keys(localStorageKeyMap).forEach((key)=>{ diff --git a/config/default.json b/config/default.json index e18e90a20..6be4ce7ce 100644 --- a/config/default.json +++ b/config/default.json @@ -1,4 +1,5 @@ { + "development": true, "host" : "homebrewery.local.naturalcrit.com:8000", "naturalcrit_url" : "local.naturalcrit.com:8010", "secret" : "secret", diff --git a/package-lock.json b/package-lock.json index fb815cdc6..ee762f8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,15 +65,15 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.35.0", + "eslint": "^9.37.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", - "globals": "^16.3.0", - "jest": "^30.1.3", + "globals": "^16.4.0", + "jest": "^30.2.0", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.24.0", + "stylelint": "^16.25.0", "stylelint-config-recess-order": "^7.3.0", "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" @@ -1915,14 +1915,14 @@ "license": "Apache-2.0" }, "node_modules/@dual-bundle/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", "dev": true, "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/JounQin" } }, "node_modules/@emnapi/core": { @@ -2026,19 +2026,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2086,9 +2089,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { @@ -2108,13 +2111,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -2214,9 +2217,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -2227,9 +2230,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -2265,9 +2268,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -2416,17 +2419,17 @@ } }, "node_modules/@jest/console": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", - "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -2434,39 +2437,39 @@ } }, "node_modules/@jest/core": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", - "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", + "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.1.3", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-resolve-dependencies": "30.1.3", - "jest-runner": "30.1.3", - "jest-runtime": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", - "jest-watcher": "30.1.3", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -2492,39 +2495,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", - "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.1.2", - "jest-snapshot": "30.1.2" + "expect": "30.2.0", + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", - "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, "license": "MIT", "dependencies": { @@ -2535,18 +2538,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", - "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2563,16 +2566,16 @@ } }, "node_modules/@jest/globals": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", - "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2593,17 +2596,17 @@ } }, "node_modules/@jest/reporters": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", - "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -2616,9 +2619,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -2696,13 +2699,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", - "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -2727,14 +2730,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", - "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -2743,15 +2746,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", - "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", + "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -2759,23 +2762,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", - "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", + "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -2786,9 +2789,9 @@ } }, "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3007,9 +3010,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -3111,13 +3114,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/stack-utils": { @@ -4045,16 +4048,16 @@ } }, "node_modules/babel-jest": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", - "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.1.2", + "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -4063,15 +4066,18 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -4084,14 +4090,12 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" }, "engines": { @@ -4182,20 +4186,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "node_modules/balanced-match": { @@ -4851,9 +4855,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -5477,9 +5481,10 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -5510,9 +5515,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6065,20 +6070,20 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6451,18 +6456,18 @@ "license": "MIT" }, "node_modules/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -7293,10 +7298,11 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -8611,16 +8617,16 @@ } }, "node_modules/jest": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", - "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", "import-local": "^3.2.0", - "jest-cli": "30.1.3" + "jest-cli": "30.2.0" }, "bin": { "jest": "bin/jest.js" @@ -8638,14 +8644,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "p-limit": "^3.1.0" }, "engines": { @@ -8653,29 +8659,29 @@ } }, "node_modules/jest-circus": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", - "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.1.0", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-runtime": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "p-limit": "^3.1.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -8685,21 +8691,21 @@ } }, "node_modules/jest-cli": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", - "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "yargs": "^17.7.2" }, "bin": { @@ -8718,34 +8724,34 @@ } }, "node_modules/jest-config": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", - "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.1.3", - "@jest/types": "30.0.5", - "babel-jest": "30.1.2", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.1.3", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.2", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-runner": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -8817,25 +8823,25 @@ } }, "node_modules/jest-diff": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", - "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", "dependencies": { @@ -8846,36 +8852,36 @@ } }, "node_modules/jest-each": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", - "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" + "jest-util": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", - "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.1.0" + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -8889,20 +8895,20 @@ "license": "MIT" }, "node_modules/jest-haste-map": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", - "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -8914,49 +8920,49 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", - "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", - "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.1.2", - "pretty-format": "30.0.5" + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", - "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -8965,15 +8971,15 @@ } }, "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -9008,18 +9014,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", - "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -9028,46 +9034,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", - "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.1.2" + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", - "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", - "@jest/environment": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.2", - "jest-haste-map": "30.1.0", - "jest-leak-detector": "30.1.0", - "jest-message-util": "30.1.0", - "jest-resolve": "30.1.3", - "jest-runtime": "30.1.3", - "jest-util": "30.0.5", - "jest-watcher": "30.1.3", - "jest-worker": "30.1.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -9076,32 +9082,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", - "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/globals": "30.1.2", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -9157,9 +9163,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", - "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, "license": "MIT", "dependencies": { @@ -9168,20 +9174,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.1.2", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.1.2", + "expect": "30.2.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.1.2", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -9203,13 +9209,13 @@ } }, "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -9234,18 +9240,18 @@ } }, "node_modules/jest-validate": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", - "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -9265,19 +9271,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", - "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "string-length": "^4.0.2" }, "engines": { @@ -9285,15 +9291,15 @@ } }, "node_modules/jest-worker": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", - "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -10608,9 +10614,9 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -11605,9 +11611,9 @@ } }, "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "license": "MIT", "dependencies": { @@ -13287,9 +13293,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.24.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.24.0.tgz", - "integrity": "sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==", + "version": "16.25.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz", + "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==", "dev": true, "funding": [ { @@ -13307,13 +13313,13 @@ "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", - "@dual-bundle/import-meta-resolve": "^4.1.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", - "debug": "^4.4.1", + "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.1.4", @@ -14162,9 +14168,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index b36b9959a..c897d842d 100644 --- a/package.json +++ b/package.json @@ -138,15 +138,15 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.35.0", + "eslint": "^9.37.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", - "globals": "^16.3.0", - "jest": "^30.1.3", + "globals": "^16.4.0", + "jest": "^30.2.0", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.24.0", + "stylelint": "^16.25.0", "stylelint-config-recess-order": "^7.3.0", "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" diff --git a/server/app.js b/server/app.js index 735ba0665..1bdb5aac3 100644 --- a/server/app.js +++ b/server/app.js @@ -35,6 +35,7 @@ import contentNegotiation from './middleware/content-negotiation.js'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; +import dbCheck from './middleware/dbCheck.js'; const sanitizeBrew = (brew, accessType)=>{ @@ -274,7 +275,7 @@ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{ app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); //User Page -app.get('/user/:username', async (req, res, next)=>{ +app.get('/user/:username', dbCheck, async (req, res, next)=>{ const ownAccount = req.account && (req.account.username == req.params.username); req.ogMeta = { ...defaultMetaTags, @@ -346,7 +347,7 @@ app.get('/user/:username', async (req, res, next)=>{ }); //Change author name on brews -app.put('/api/user/rename', async (req, res)=>{ +app.put('/api/user/rename', dbCheck, async (req, res)=>{ const { username, newUsername } = req.body; const ownAccount = req.account && (req.account.username == newUsername); @@ -432,7 +433,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{ })); //Share Page -app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ +app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ const { brew } = req; req.ogMeta = { ...defaultMetaTags, title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, @@ -459,7 +460,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r })); //Account Page -app.get('/account', asyncHandler(async (req, res, next)=>{ +app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ const data = {}; data.title = 'Account Information Page'; diff --git a/server/db.js b/server/db.js index 196eba650..4930e4cd6 100644 --- a/server/db.js +++ b/server/db.js @@ -22,6 +22,14 @@ const handleConnectionError = (error)=>{ } }; +const addListeners = (conn)=>{ + conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');}); + conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');}); + conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');}); + conn.connection.on('connected', ()=>{console.log('Mongo connected!');}); + return conn; +}; + const disconnect = async ()=>{ return await Mongoose.disconnect(); }; @@ -31,10 +39,12 @@ const connect = async (config)=>{ retryWrites : false, autoIndex : (config.get('local_environments').includes(config.get('node_env'))) }) - .catch((error)=>handleConnectionError(error)); + .then(addListeners(Mongoose)) + .catch((error)=>handleConnectionError(error)); }; export default { connect, disconnect }; + diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 3221638ab..c931cb657 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -8,11 +8,12 @@ import Markdown from '../shared/naturalcrit/markdown.js'; import yaml from 'js-yaml'; import asyncHandler from 'express-async-handler'; import { nanoid } from 'nanoid'; -import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch'; +import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch'; import { md5 } from 'hash-wasm'; -import { splitTextStyleAndMetadata, +import { splitTextStyleAndMetadata, brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; import checkClientVersion from './middleware/check-client-version.js'; +import dbCheck from './middleware/dbCheck.js'; const router = express.Router(); @@ -377,14 +378,14 @@ const api = { // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); if(patchedResult != brewFromClient.text) - throw("Patches did not apply cleanly, text mismatch detected"); + throw ('Patches did not apply cleanly, text mismatch detected'); // brew.text = applyPatches(patches, brewFromServer.text)[0]; } catch (err) { //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); console.error('Failed to apply patches:', { //patches : brewFromClient.patches, - brewId : brewFromClient.editId || 'unknown', - error : err + brewId : brewFromClient.editId || 'unknown', + error : err }); // While running in parallel, don't throw the error upstream. // throw err; // rethrow to preserve the 500 behavior @@ -530,6 +531,8 @@ const api = { } }; +router.use(dbCheck); + router.post('/api', checkClientVersion, asyncHandler(api.newBrew)); router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); diff --git a/server/middleware/dbCheck.js b/server/middleware/dbCheck.js new file mode 100644 index 000000000..f486eab52 --- /dev/null +++ b/server/middleware/dbCheck.js @@ -0,0 +1,15 @@ +import mongoose from 'mongoose'; +import config from '../config.js'; + +export default (req, res, next)=>{ + // Bypass DB checks during testing + if(config.get('node_env') == 'test') return next(); + + if(mongoose.connection.readyState == 1) return next(); + throw { + HBErrorCode : '13', + name : 'Database Connection Error', + message : 'Unable to connect to database', + status : mongoose.connection.readyState + }; +}; diff --git a/server/middleware/dbCheck.spec.js b/server/middleware/dbCheck.spec.js new file mode 100644 index 000000000..0c37d40ab --- /dev/null +++ b/server/middleware/dbCheck.spec.js @@ -0,0 +1,28 @@ +import mongoose from 'mongoose'; +import dbCheck from './dbCheck.js'; +import config from '../config.js'; + +describe('dbCheck middleware', ()=>{ + const next = jest.fn(); + + afterEach(()=>jest.clearAllMocks()); + + it('should skip check in test mode', ()=>{ + config.get = jest.fn(()=>'test'); + expect(()=>dbCheck({}, {}, next)).not.toThrow(); + expect(next).toHaveBeenCalled(); + }); + + it('should call next if readyState == 1', ()=>{ + config.get = jest.fn(()=>'production'); + mongoose.connection.readyState = 1; + dbCheck({}, {}, next); + expect(next).toHaveBeenCalled(); + }); + + it('should throw if readyState != 1', ()=>{ + config.get = jest.fn(()=>'production'); + mongoose.connection.readyState = 99; + expect(()=>dbCheck({}, {}, next)).toThrow(/Unable to connect/); + }); +}); \ No newline at end of file diff --git a/shared/helpers.js b/shared/helpers.js index 3f91583d6..adf5b889a 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul const mpAsSnippets = []; // Snippets from Themes first. if(themeBundleSnippets) { - for (let themes of themeBundleSnippets) { + for (const themes of themeBundleSnippets) { if(typeof themes !== 'string') { const userSnippets = []; const snipSplit = themes.snippets.trim().split(textSplit).slice(1); @@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{ if(typeof yamlObj == 'string') return yamlObj; let snippetsText = ''; - - for (let snippet of yamlObj) { - for (let subSnippet of snippet.subsnippets) { + + for (const snippet of yamlObj) { + for (const subSnippet of snippet.subsnippets) { snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; } } @@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ const res = await request .get(`/api/theme/${renderer}/${theme}`) .catch((err)=>{ - setError(err) + setError(err); }); if(!res) { setThemeBundle({}); @@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ setError(null); }; -const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { +const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{ const clientText = clientTextRaw?.normalize('NFC') || ''; const serverText = serverTextRaw?.normalize('NFC') || ''; const clientBuffer = Buffer.from(clientText, 'utf8'); const serverBuffer = Buffer.from(serverText, 'utf8'); - if (clientBuffer.equals(serverBuffer)) { + if(clientBuffer.equals(serverBuffer)) { console.log(`✅ ${label} text matches byte-for-byte.`); return; } @@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { // Byte-level diff for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) { - if (clientBuffer[i] !== serverBuffer[i]) { + if(clientBuffer[i] !== serverBuffer[i]) { console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`); break; } @@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { // Char-level diff for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) { - if (clientText[i] !== serverText[i]) { + if(clientText[i] !== serverText[i]) { console.log(`Char mismatch at index ${i}:`); console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`); console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`); break; } } -} +}; export { splitTextStyleAndMetadata, diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 78107dcf4..b0b44a079 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) { try { return mathParser.evaluate(replacedLabel); - } catch (error) { + } catch { return undefined; // Return undefined if invalid math result } } @@ -680,7 +680,7 @@ const tableTerminators = [ Marked.use(MarkedVariables()); Marked.use(MarkedDefinitionLists()); -Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] }); +Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use(mustacheInjectBlock); Marked.use(MarkedAlignedParagraphs()); Marked.use(MarkedSubSuperText()); diff --git a/shared/naturalcrit/markdownLegacy.js b/shared/naturalcrit/markdownLegacy.js index a6a37d639..5a8108297 100644 --- a/shared/naturalcrit/markdownLegacy.js +++ b/shared/naturalcrit/markdownLegacy.js @@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) { prot = decodeURIComponent(unescape(href)) .replace(nonWordAndColonTest, '') .toLowerCase(); - } catch (e) { + } catch { return null; } if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { @@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) { } try { href = encodeURI(href).replace(/%25/g, '%'); - } catch (e) { + } catch { return null; } return href; diff --git a/themes/V3/Blank/style.less b/themes/V3/Blank/style.less index 42a392454..9d41181e9 100644 --- a/themes/V3/Blank/style.less +++ b/themes/V3/Blank/style.less @@ -611,3 +611,17 @@ h6, } .toc.wide li { break-inside : auto; } } + + +/********************************** +Firefox endruns +**********************************/ + +@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...` + .page { + blockquote, table { + page-break-inside: auto; + break-inside: auto; + } + } +} \ No newline at end of file