From 826e6cba7032341ade6e56a46af31f9a61d8a14f Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 23 Dec 2024 14:51:22 -0600 Subject: [PATCH 01/50] New single-pass algorithm. Co-authored-by: MichaelWest22 --- .github/workflows/ci.yml | 34 - CONCEPT.md | 31 +- README.md | 11 - TESTING.md | 6 +- src/idiomorph.js | 672 ++++++++++--------- test/bootstrap.js | 33 +- test/index.html | 2 +- test/{two-pass.js => retain-hidden-state.js} | 338 +++++++--- web-test-runner.config.mjs | 2 - 9 files changed, 648 insertions(+), 481 deletions(-) rename test/{two-pass.js => retain-hidden-state.js} (58%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8079dca..6b81520 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,40 +33,6 @@ jobs: - name: Run tests run: npm run test-move-before - test-force-two-pass: - runs-on: ubuntu-latest - env: - DEFAULT_TWO_PASS: true - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - cache: 'npm' - - name: Install dependencies - run: npm install - - name: Install browsers - run: npx playwright install --with-deps - - name: Run tests - run: npm run ci - - test-force-two-pass-move-before: - runs-on: ubuntu-latest - env: - DEFAULT_TWO_PASS: true - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - cache: 'npm' - - name: Install dependencies - run: npm install - - name: Run tests - run: npm run test-move-before - typecheck: runs-on: ubuntu-latest steps: diff --git a/CONCEPT.md b/CONCEPT.md index 4a29bf2..6c1b833 100644 --- a/CONCEPT.md +++ b/CONCEPT.md @@ -87,7 +87,7 @@ Given a old node N, we can ask if it might match _any_ child of a parent node P Given these two enhancements: * The ability to ask if two nodes are the same based on child information efficiently -* The abilit to ask if a node has a match within an element efficiently +* The ability to ask if a node has a match within an element efficiently We believe we can improve on the fidelity of DOM matching when compared with plain ID-based matching. @@ -105,23 +105,30 @@ Sync the attributes of newNode onto oldNode Let insertionPoint be the first child of the oldNode while newNode has children let newChild be the result of shifting the first child from newNodes children - if the newChild is the same as the insertionPoint + if the newChild matches the insertionPoint recursively merge newChild into insertionPoint advance the insertionPoint to its next sibling - else if the newChild has a match within the oldNode - scan the insertionPoint forward to the match, discarding children of the old node - recursively merge newChild into insertionPoint - advance the insertionPoint to its next sibling - else if the insertionPoint has a match within the newNode - insert the newChild before the insertionPoint - else if the insertionPoint and the newChild are compatible + else if the newChild has a match among oldNodes children + move the match before the insertionPoint + recursively merge newChild into the match + else if the newChild and insertionPoint are compatible recursively merge newChild into insertionPoint advance the insertionPoint to its next sibling + else if the newChild has a compatible match among oldNodes children + move the match before the insertionPoint + recursively merge newChild into the match + else if the newChild is IDed and has a match later in the oldTree or pantry + move the match before the insertionPoint + recursively merge newChild into the match else - insert the newChild before the insertionPoint + insert a clone of the newChild before the insertionPoint end while insertionPoint is not null - remove the insertion point and advance to the next sibling + if the insertionPoint is IDed and has a match later in the newTree + move the insertionPoint into a temporary pantry div for later reuse + else + delete the insertionPoint + advance the insertionPoint to its next sibling -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index e9b7832..2274259 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,6 @@ Idiomorph supports the following options: | `ignoreActiveValue` | If set to `true`, idiomorph will not update the active element's value | `Idiomorph.morph(..., {ignoreActiveValue:true})` | | `head` | Allows you to control how the `head` tag is merged. See the [head](#the-head-tag) section for more details | `Idiomorph.morph(..., {head:{style:merge}})` | | `callbacks` | Allows you to insert callbacks when events occur in the morph life cycle, see the callback table below | `Idiomorph.morph(..., {callbacks:{beforeNodeAdded:function(node){...}})` | -| `twoPass` | If set to `true`, idiomorph does a second pass to maintain more state. See [Two Pass Mode](#two-pass-mode) | `Idiomorph.morph(..., {twoPass:true})` | #### Callbacks @@ -112,7 +111,6 @@ of the algorithm. | beforeNodeRemoved(node) | Called before a node is removed from the DOM | return false to not remove the node | | afterNodeRemoved(node) | Called after a node is removed from the DOM | none | | beforeAttributeUpdated(attributeName, node, mutationType) | Called before an attribute on an element. `mutationType` is either "updated" or "removed"| return false to not update or remove the attribute | -| beforeNodePantried(node) | Called before moving the node into the pantry during the second pass (if enabled) | return false to not move the node into the pantry | ### The `head` tag @@ -142,15 +140,6 @@ of the script. Similarly, you may wish to preserve an element even if it is not in `new`. You can use the attribute `im-preserve='true'` in this case to retain the element. -#### Two Pass Mode - -If the `twoPass` option is enabled, Idiomorph will try to maintain more state by temporarily moving some nodes into a -hidden pantry div. After the initial morph is complete, it runs a second pass, moving the pantried nodes back into the -document. These are nodes that would otherwise have been removed and readded, losing identity and state. This is -particularly useful for reordering morphs involving elements with non-attribute state, e.g. video elements, elements -preserved with `data-turbo-permanent`, etc. Also, when this option is enabled, Idiomorph will use the upcoming -`Element#moveBefore` API if it exists, falling back to `Element#insertBefore` if not. - #### Additional Configuration You are also able to override these behaviors, see the `head` config object in the source code. diff --git a/TESTING.md b/TESTING.md index 3b12e43..aa95604 100644 --- a/TESTING.md +++ b/TESTING.md @@ -54,16 +54,12 @@ npm run debug ``` This will start the server, and open the test runner in a browser. From there you can choose a test file to run. -## Forcing Two Pass Mode -If the `DEFAULT_TWO_PASS` environment variable is set before running the tests, Idiomorph will default to two-pass mode. This is useful for running the entire test suite with two-pass on. - ## GitHub Actions CI matrix On each push and PR, GitHub Actions runs the following test matrix: 1. Normal baseline `npm run ci` run 2. With experimental moveBefore enabled in the browser -3. With two-pass mode forced -4. With both moveBefore enabled and two-pass mode forced +3. Typecheck ## Code Coverage Report After a test run completes, you can open `coverage/lcov-report/index.html` to view the code coverage report. On Ubuntu you can run: diff --git a/src/idiomorph.js b/src/idiomorph.js index a58ee8c..ec190bc 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -20,7 +20,6 @@ * @property {function(Element): boolean} [beforeNodeRemoved] * @property {function(Element): void} [afterNodeRemoved] * @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated] - * @property {function(Element): boolean} [beforeNodePantried] */ /** @@ -61,7 +60,6 @@ * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved * @property {(function(Node): void) | NoOp} afterNodeRemoved * @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated - * @property {(function(Node): boolean) | NoOp} beforeNodePantried */ /** @@ -72,7 +70,6 @@ * @property {boolean} [ignoreActiveValue] * @property {ConfigCallbacksInternal} callbacks * @property {ConfigHeadInternal} head - * @property {boolean} [twoPass] */ /** @@ -95,8 +92,8 @@ var Idiomorph = (function () { /** * @typedef {object} MorphContext * - * @property {Node} target - * @property {Node} newContent + * @property {Element} target + * @property {Element} newContent * @property {ConfigInternal} config * @property {ConfigInternal['morphStyle']} morphStyle * @property {ConfigInternal['ignoreActive']} ignoreActive @@ -133,7 +130,6 @@ var Idiomorph = (function () { beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp, - beforeNodePantried: noOp, }, head: { style: "merge", @@ -186,7 +182,7 @@ var Idiomorph = (function () { let oldHead = oldNode.querySelector("head"); let newHead = normalizedNewContent.querySelector("head"); if (oldHead && newHead) { - let promises = handleHeadElement(newHead, oldHead, ctx); + let promises = handleHeadElement(oldHead, newHead, ctx); // when head promises resolve, call morph again, ignoring the head tag Promise.all(promises).then(function () { morphNormalizedContent( @@ -206,15 +202,13 @@ var Idiomorph = (function () { if (ctx.morphStyle === "innerHTML") { // innerHTML, so we are only updating the children - morphChildren(normalizedNewContent, oldNode, ctx); - if (ctx.config.twoPass) { - restoreFromPantry(oldNode, ctx); - } + morphChildren(oldNode, normalizedNewContent, ctx); + ctx.pantry.remove(); return Array.from(oldNode.children); } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { // otherwise find the best element match in the new content, morph that, and merge its siblings // into either side of the best match - let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); + let bestMatch = findBestNodeMatch(oldNode, normalizedNewContent, ctx); // stash the siblings that will need to be inserted on either side of the best match let previousSibling = bestMatch?.previousSibling ?? null; @@ -231,10 +225,9 @@ var Idiomorph = (function () { previousSibling, morphedNode, nextSibling, + ctx, ); - if (ctx.config.twoPass) { - restoreFromPantry(morphedNode.parentNode, ctx); - } + ctx.pantry.remove(); return elements; } } else { @@ -263,6 +256,41 @@ var Idiomorph = (function () { ); } + /** + * @param {Element | null} oldParent + * @param {Node} newChild new content to merge + * @param {Node | null} insertionPoint insertion point to place content before + * @param {MorphContext} ctx the merge context + */ + function insertOrMorphNode(oldParent, newChild, insertionPoint, ctx) { + if (oldParent == null) return; + if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { + // this node id is somewhere so move it and all its children here and then morph + const movedChild = moveBeforeById( + oldParent, + /** @type {Element} */ (newChild).id, + insertionPoint, + ctx, + ); + morphOldNodeTo(movedChild, newChild, ctx); + } else { + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { + // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm + const newEmptyChild = document.createElement(newChild.tagName); + oldParent.insertBefore(newEmptyChild, insertionPoint); + morphOldNodeTo(newEmptyChild, newChild, ctx); + ctx.callbacks.afterNodeAdded(newEmptyChild); + } else { + // no id state to preserve so just insert a clone of the new data to avoid mutating newParent + const newClonedChild = document.importNode(newChild, true); + oldParent.insertBefore(newClonedChild, insertionPoint); + ctx.callbacks.afterNodeAdded(newClonedChild); + } + } + removeIdsFromConsideration(ctx, newChild); + } + /** * @param {Node} oldNode root node to merge content into * @param {Node | null} newContent new content to merge @@ -273,19 +301,13 @@ var Idiomorph = (function () { if (ctx.ignoreActive && oldNode === document.activeElement) { // don't morph focused element } else if (newContent == null) { - if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; - - oldNode.parentNode?.removeChild(oldNode); - ctx.callbacks.afterNodeRemoved(oldNode); + removeNode(oldNode, ctx); return null; } else if (!isSoftMatch(oldNode, newContent)) { - if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; - if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; - - oldNode.parentNode?.replaceChild(newContent, oldNode); - ctx.callbacks.afterNodeAdded(newContent); - ctx.callbacks.afterNodeRemoved(oldNode); - return newContent; + insertOrMorphNode(oldNode.parentElement, newContent, oldNode, ctx); + const newNode = oldNode.previousSibling; + removeNode(oldNode, ctx); + return newNode; } else { if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; @@ -298,14 +320,15 @@ var Idiomorph = (function () { ) { // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above handleHeadElement( - /** @type {HTMLHeadElement} */ (newContent), oldNode, + /** @type {HTMLHeadElement} */ (newContent), ctx, ); } else { - syncNodeFrom(newContent, oldNode, ctx); + syncNode(oldNode, newContent, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { - morphChildren(newContent, oldNode, ctx); + // @ts-ignore newContent can be a node here because .firstChild will be null + morphChildren(oldNode, newContent, ctx); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); @@ -314,6 +337,24 @@ var Idiomorph = (function () { return null; } + /** + * @param {Node} oldNode the node to be morphed + * @param {Node} newChild the new content + * @param {Node|null} insertionPoint the current point in the DOM we are morphing content at + * @param {MorphContext} ctx the merge context + * @returns {Node|null} returns the new insertion point after the merged node + */ + function morphChild(oldNode, newChild, insertionPoint, ctx) { + // if the node to morph is not at the insertion point then we need to move it here by moving or removing nodes + if (oldNode !== insertionPoint) { + // @ts-ignore we know the Node has a valid parent + moveBefore(oldNode.parentElement, oldNode, insertionPoint); + } + morphOldNodeTo(oldNode, newChild, ctx); + removeIdsFromConsideration(ctx, newChild); + return oldNode.nextSibling; + } + /** * This is the core algorithm for matching up children. The idea is to use id sets to try to match up * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but @@ -321,127 +362,106 @@ var Idiomorph = (function () { * * Basic algorithm is, for each node in the new content: * - * - if we have reached the end of the old parent, append the new content - * - if the new content has an id set match with the current insertion point, morph - * - search for an id set match - * - if id set match found, morph - * - otherwise search for a "soft" match - * - if a soft match is found, morph + * - if we have not reached the end of the old parent: + * - if the new content has an id set match with the current insertion point, morph + * - search for an id set match + * - if id set match found, morph + * - if the new content is a soft match with the current insertion point, morph + * - otherwise search for a "soft" match + * - if a soft match is found, morph * - otherwise, prepend the new node before the current insertion point * * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch() and findSoftMatch() for details. * - * @param {Node} newParent the parent element of the new content - * @param {Node} oldParent the old content that we are merging the new content into + * @param {Element} newParent the parent element of the new content + * @param {Element} oldParent the old content that we are merging the new content into * @param {MorphContext} ctx the merge context * @returns {void} */ - function morphChildren(newParent, oldParent, ctx) { + function morphChildren(oldParent, newParent, ctx) { if ( - newParent instanceof HTMLTemplateElement && - oldParent instanceof HTMLTemplateElement + oldParent instanceof HTMLTemplateElement && + newParent instanceof HTMLTemplateElement ) { - newParent = newParent.content; + // @ts-ignore we can pretend the DocumentFragment is an Element oldParent = oldParent.content; + // @ts-ignore ditto + newParent = newParent.content; } - /** - * - * @type {Node | null} - */ - let nextNewChild = newParent.firstChild; - /** - * - * @type {Node | null} - */ - let insertionPoint = oldParent.firstChild; - let newChild; + let insertionPoint = /** @type {Node | null} */ (oldParent.firstChild); // run through all the new content - while (nextNewChild) { - newChild = nextNewChild; - nextNewChild = newChild.nextSibling; - - // if we are at the end of the exiting parent's children, just append - if (insertionPoint == null) { - // skip add callbacks when we're going to be restoring this from the pantry in the second pass - if ( - ctx.config.twoPass && - ctx.persistentIds.has(/** @type {Element} */ (newChild).id) - ) { - oldParent.appendChild(newChild); - } else { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; - oldParent.appendChild(newChild); - ctx.callbacks.afterNodeAdded(newChild); - } - removeIdsFromConsideration(ctx, newChild); - continue; - } - + for (const newChild of newParent.childNodes) { // if the current node has an id set match then morph if (isIdSetMatch(newChild, insertionPoint, ctx)) { - morphOldNodeTo(insertionPoint, newChild, ctx); - insertionPoint = insertionPoint.nextSibling; - removeIdsFromConsideration(ctx, newChild); + insertionPoint = morphChild( + /** @type {Node} */ (insertionPoint), + newChild, + insertionPoint, + ctx, + ); continue; } // otherwise search forward in the existing old children for an id set match - let idSetMatch = findIdSetMatch( - newParent, + const idSetMatch = findIdSetMatch( oldParent, + newParent, newChild, insertionPoint, ctx, ); - - // if we found a potential match, remove the nodes until that point and morph if (idSetMatch) { - insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); - morphOldNodeTo(idSetMatch, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); + insertionPoint = morphChild(idSetMatch, newChild, insertionPoint, ctx); continue; } - // no id set match found, so scan forward for a soft match for the current node - let softMatch = findSoftMatch( - newParent, + // if the current point is already a soft match morph + if (isSoftMatch(insertionPoint, newChild)) { + insertionPoint = morphChild( + /** @type {Node} */ (insertionPoint), + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // search forward in the existing old children for a soft match for the current node + const softMatch = findSoftMatch( oldParent, + newParent, newChild, insertionPoint, ctx, ); - - // if we found a soft match for the current node, morph if (softMatch) { - insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); - morphOldNodeTo(softMatch, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); + insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); continue; } - // abandon all hope of morphing, just insert the new child before the insertion point - // and move on - - // skip add callbacks when we're going to be restoring this from the pantry in the second pass - if ( - ctx.config.twoPass && - ctx.persistentIds.has(/** @type {Element} */ (newChild).id) - ) { - oldParent.insertBefore(newChild, insertionPoint); - } else { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) continue; - oldParent.insertBefore(newChild, insertionPoint); - ctx.callbacks.afterNodeAdded(newChild); + // match is somewhere else in the document, find it and move it + if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { + const movedChild = moveBeforeById( + oldParent, + /** @type {Element} */ (newChild).id, + insertionPoint, + ctx, + ); + morphOldNodeTo(movedChild, newChild, ctx); + removeIdsFromConsideration(ctx, newChild); + continue; } - removeIdsFromConsideration(ctx, newChild); + + // last resort, create a new node from scratch + insertOrMorphNode(oldParent, newChild, insertionPoint, ctx); } // remove any remaining old nodes that didn't match up with new content - while (insertionPoint !== null) { - let tempNode = insertionPoint; + while (insertionPoint) { + const tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } @@ -453,107 +473,77 @@ var Idiomorph = (function () { /** * @param {string} attr the attribute to be mutated - * @param {Element} to the element that is going to be updated + * @param {Element} element the element that is going to be updated * @param {"update" | "remove"} updateType * @param {MorphContext} ctx the merge context * @returns {boolean} true if the attribute should be ignored, false otherwise */ - function ignoreAttribute(attr, to, updateType, ctx) { + function ignoreAttribute(attr, element, updateType, ctx) { if ( attr === "value" && ctx.ignoreActiveValue && - to === document.activeElement + element === document.activeElement ) { return true; } - return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; + return ( + ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false + ); } /** * syncs a given node with another node, copying over all attributes and - * inner element state from the 'from' node to the 'to' node + * inner element state from the newNode to the oldNode * - * @param {Node} from the element to copy attributes & state from - * @param {Node} to the element to copy attributes & state to + * @param {Node} oldNode the node to copy attributes & state to + * @param {Node} newNode the node to copy attributes & state from * @param {MorphContext} ctx the merge context */ - function syncNodeFrom(from, to, ctx) { - let type = from.nodeType; + + function syncNode(oldNode, newNode, ctx) { + let type = newNode.nodeType; // if is an element type, sync the attributes from the // new node into the new node if (type === 1 /* element type */) { - const fromEl = /** @type {Element} */ (from); - const toEl = /** @type {Element} */ (to); - const fromAttributes = fromEl.attributes; - const toAttributes = toEl.attributes; - for (const fromAttribute of fromAttributes) { - if (ignoreAttribute(fromAttribute.name, toEl, "update", ctx)) { + const oldElt = /** @type {Element} */ (oldNode); + const newElt = /** @type {Element} */ (newNode); + + const oldAttributes = oldElt.attributes; + const newAttributes = newElt.attributes; + for (const newAttribute of newAttributes) { + if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { continue; } - if (toEl.getAttribute(fromAttribute.name) !== fromAttribute.value) { - toEl.setAttribute(fromAttribute.name, fromAttribute.value); + if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) { + oldElt.setAttribute(newAttribute.name, newAttribute.value); } } // iterate backwards to avoid skipping over items when a delete occurs - for (let i = toAttributes.length - 1; 0 <= i; i--) { - const toAttribute = toAttributes[i]; + for (let i = oldAttributes.length - 1; 0 <= i; i--) { + const oldAttribute = oldAttributes[i]; // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe // e.g. custom element attribute callbacks can remove other attributes - if (!toAttribute) continue; + if (!oldAttribute) continue; - if (!fromEl.hasAttribute(toAttribute.name)) { - if (ignoreAttribute(toAttribute.name, toEl, "remove", ctx)) { + if (!newElt.hasAttribute(oldAttribute.name)) { + if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) { continue; } - toEl.removeAttribute(toAttribute.name); + oldElt.removeAttribute(oldAttribute.name); } } - } - // sync text nodes - if (type === 8 /* comment */ || type === 3 /* text */) { - if (to.nodeValue !== from.nodeValue) { - to.nodeValue = from.nodeValue; + if (!ignoreValueOfActiveElement(oldElt, ctx)) { + syncInputValue(oldElt, newElt, ctx); } } - if (!ignoreValueOfActiveElement(to, ctx)) { - // sync input values - syncInputValue(from, to, ctx); - } - } - - /** - * @param {Element} from element to sync the value from - * @param {Element} to element to sync the value to - * @param {string} attributeName the attribute name - * @param {MorphContext} ctx the merge context - */ - function syncBooleanAttribute(from, to, attributeName, ctx) { - // TODO: prefer set/getAttribute here - if (!(from instanceof Element && to instanceof Element)) return; - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - const fromLiveValue = from[attributeName], - // @ts-ignore ditto - toLiveValue = to[attributeName]; - if (fromLiveValue !== toLiveValue) { - let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx); - if (!ignoreUpdate) { - // update attribute's associated DOM property - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - to[attributeName] = from[attributeName]; - } - if (fromLiveValue) { - if (!ignoreUpdate) { - // TODO: do we really want this? tests say so but it feels wrong - to.setAttribute(attributeName, fromLiveValue); - } - } else { - if (!ignoreAttribute(attributeName, to, "remove", ctx)) { - to.removeAttribute(attributeName); - } + // sync text nodes + if (type === 8 /* comment */ || type === 3 /* text */) { + if (oldNode.nodeValue !== newNode.nodeValue) { + oldNode.nodeValue = newNode.nodeValue; } } } @@ -564,55 +554,94 @@ var Idiomorph = (function () { * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 * - * @param {Node} from the element to sync the input value from - * @param {Node} to the element to sync the input value to + * @param {Element} oldElement the element to sync the input value to + * @param {Element} newElement the element to sync the input value from * @param {MorphContext} ctx the merge context */ - function syncInputValue(from, to, ctx) { + function syncInputValue(oldElement, newElement, ctx) { if ( - from instanceof HTMLInputElement && - to instanceof HTMLInputElement && - from.type !== "file" + oldElement instanceof HTMLInputElement && + newElement instanceof HTMLInputElement && + newElement.type !== "file" ) { - let fromValue = from.value; - let toValue = to.value; + let newValue = newElement.value; + let oldValue = oldElement.value; // sync boolean attributes - syncBooleanAttribute(from, to, "checked", ctx); - syncBooleanAttribute(from, to, "disabled", ctx); + syncBooleanAttribute(oldElement, newElement, "checked", ctx); + syncBooleanAttribute(oldElement, newElement, "disabled", ctx); - if (!from.hasAttribute("value")) { - if (!ignoreAttribute("value", to, "remove", ctx)) { - to.value = ""; - to.removeAttribute("value"); + if (!newElement.hasAttribute("value")) { + if (!ignoreAttribute("value", oldElement, "remove", ctx)) { + oldElement.value = ""; + oldElement.removeAttribute("value"); } - } else if (fromValue !== toValue) { - if (!ignoreAttribute("value", to, "update", ctx)) { - to.setAttribute("value", fromValue); - to.value = fromValue; + } else if (oldValue !== newValue) { + if (!ignoreAttribute("value", oldElement, "update", ctx)) { + oldElement.setAttribute("value", newValue); + oldElement.value = newValue; } } - // TODO: QUESTION(1cg): this used to only check `from` unlike the other branches -- why? + // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why? // did I break something? } else if ( - from instanceof HTMLOptionElement && - to instanceof HTMLOptionElement + oldElement instanceof HTMLOptionElement && + newElement instanceof HTMLOptionElement ) { - syncBooleanAttribute(from, to, "selected", ctx); + syncBooleanAttribute(oldElement, newElement, "selected", ctx); } else if ( - from instanceof HTMLTextAreaElement && - to instanceof HTMLTextAreaElement + oldElement instanceof HTMLTextAreaElement && + newElement instanceof HTMLTextAreaElement ) { - let fromValue = from.value; - let toValue = to.value; - if (ignoreAttribute("value", to, "update", ctx)) { + let newValue = newElement.value; + let oldValue = oldElement.value; + if (ignoreAttribute("value", oldElement, "update", ctx)) { return; } - if (fromValue !== toValue) { - to.value = fromValue; + if (newValue !== oldValue) { + oldElement.value = newValue; + } + if ( + oldElement.firstChild && + oldElement.firstChild.nodeValue !== newValue + ) { + oldElement.firstChild.nodeValue = newValue; + } + } + } + + /** + * @param {Element} oldElement element to sync the value to + * @param {Element} newElement element to sync the value from + * @param {string} attributeName the attribute name + * @param {MorphContext} ctx the merge context + */ + function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + const newLiveValue = newElement[attributeName], + // @ts-ignore ditto + oldLiveValue = oldElement[attributeName]; + if (newLiveValue !== oldLiveValue) { + const ignoreUpdate = ignoreAttribute( + attributeName, + oldElement, + "update", + ctx, + ); + if (!ignoreUpdate) { + // update attribute's associated DOM property + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + oldElement[attributeName] = newElement[attributeName]; } - if (to.firstChild && to.firstChild.nodeValue !== fromValue) { - to.firstChild.nodeValue = fromValue; + if (newLiveValue) { + if (!ignoreUpdate) { + // TODO: do we really want this? tests say so but it feels wrong + oldElement.setAttribute(attributeName, newLiveValue); + } + } else { + if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { + oldElement.removeAttribute(attributeName); + } } } } @@ -621,12 +650,12 @@ var Idiomorph = (function () { * ============================================================================= * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style * ============================================================================= - * @param {Element} newHeadTag - * @param {Element} currentHead + * @param {Element} oldHead + * @param {Element} newHead * @param {MorphContext} ctx * @returns {Promise[]} */ - function handleHeadElement(newHeadTag, currentHead, ctx) { + function handleHeadElement(oldHead, newHead, ctx) { /** * @type {Node[]} */ @@ -648,12 +677,12 @@ var Idiomorph = (function () { // put all new head elements into a Map, by their outerHTML let srcToNewHeadNodes = new Map(); - for (const newHeadChild of newHeadTag.children) { + for (const newHeadChild of newHead.children) { srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); } // for each elt in the current head - for (const currentHeadElt of currentHead.children) { + for (const currentHeadElt of oldHead.children) { // If the current head element is in the map let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); let isReAppended = ctx.head.shouldReAppend(currentHeadElt); @@ -713,7 +742,7 @@ var Idiomorph = (function () { }); promises.push(promise); } - currentHead.appendChild(newElt); + oldHead.appendChild(newElt); ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); } @@ -722,13 +751,10 @@ var Idiomorph = (function () { // remove all removed elements, after we have appended the new elements to avoid // additional network requests for things like style sheets for (const removedElement of removed) { - if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { - currentHead.removeChild(removedElement); - ctx.callbacks.afterNodeRemoved(removedElement); - } + removeNode(removedElement, ctx); } - ctx.head.afterHeadMorphed(currentHead, { + ctx.head.afterHeadMorphed(oldHead, { added: added, kept: preserved, removed: removed, @@ -795,12 +821,8 @@ var Idiomorph = (function () { ignoreActiveValue: mergedConfig.ignoreActiveValue, idMap: createIdMap(oldNode, newContent), deadIds: new Set(), - persistentIds: mergedConfig.twoPass - ? createPersistentIds(oldNode, newContent) - : new Set(), - pantry: mergedConfig.twoPass - ? createPantry() - : document.createElement("div"), + persistentIds: createPersistentIds(oldNode, newContent), + pantry: createPantry(), callbacks: mergedConfig.callbacks, head: mergedConfig.head, }; @@ -863,26 +885,6 @@ var Idiomorph = (function () { ); } - /** - * - * @param {Node} startInclusive - * @param {Node} endExclusive - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function removeNodesBetween(startInclusive, endExclusive, ctx) { - /** @type {Node | null} */ let cursor = startInclusive; - while (cursor !== endExclusive) { - let tempNode = /** @type {Node} */ (cursor); - // TODO: Prefer assigning to a new variable here or expand the type of startInclusive - // to be Node | null - cursor = tempNode.nextSibling; - removeNode(tempNode, ctx); - } - removeIdsFromConsideration(ctx, endExclusive); - return endExclusive.nextSibling; - } - /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential id match @@ -890,20 +892,22 @@ var Idiomorph = (function () { * if the number of potential id matches we are discarding is greater than the * potential id matches for the new child * ============================================================================= - * @param {Node} newContent * @param {Node} oldParent + * @param {Node} newContent * @param {Node} newChild - * @param {Node} insertionPoint + * @param {Node | null} insertionPoint * @param {MorphContext} ctx - * @returns {null | Node} + * @returns {Node | null} */ function findIdSetMatch( - newContent, oldParent, + newContent, newChild, insertionPoint, ctx, ) { + if (!insertionPoint) return null; + // max id matches we are willing to discard in our search let newChildPotentialIdCount = getIdIntersectionCount( ctx, @@ -958,14 +962,14 @@ var Idiomorph = (function () { * if we find a potential id match in the old parents children OR if we find two * potential soft matches for the next two pieces of new content * ============================================================================= - * @param {Node} newContent * @param {Node} oldParent + * @param {Node} newContent * @param {Node} newChild - * @param {Node} insertionPoint + * @param {Node | null} insertionPoint * @param {MorphContext} ctx * @returns {null | Node} */ - function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { + function findSoftMatch(oldParent, newContent, newChild, insertionPoint, ctx) { /** * @type {Node | null} */ @@ -1094,9 +1098,10 @@ var Idiomorph = (function () { * @param {Node | null} previousSibling * @param {Node} morphedNode * @param {Node | null} nextSibling + * @param {MorphContext} ctx * @returns {Node[]} */ - function insertSiblings(previousSibling, morphedNode, nextSibling) { + function insertSiblings(previousSibling, morphedNode, nextSibling, ctx) { /** * @type {Node[]} */ @@ -1114,7 +1119,7 @@ var Idiomorph = (function () { let node = stack.pop(); while (node !== undefined) { added.push(node); // push added preceding siblings on in order and insert - morphedNode.parentElement?.insertBefore(node, morphedNode); + insertOrMorphNode(morphedNode.parentElement, node, morphedNode, ctx); node = stack.pop(); } added.push(morphedNode); @@ -1125,54 +1130,59 @@ var Idiomorph = (function () { } while (stack.length > 0) { const node = /** @type {Node} */ (stack.pop()); - morphedNode.parentElement?.insertBefore(node, morphedNode.nextSibling); + insertOrMorphNode( + morphedNode.parentElement, + node, + morphedNode.nextSibling, + ctx, + ); } return added; } /** * + * @param {Element} oldElement * @param {Element} newContent - * @param {Element} oldNode * @param {MorphContext} ctx * @returns {Node | null} */ - function findBestNodeMatch(newContent, oldNode, ctx) { + function findBestNodeMatch(oldElement, newContent, ctx) { /** * @type {Node | null} */ - let currentElement; - currentElement = newContent.firstChild; + let currentNode; + currentNode = newContent.firstChild; /** * @type {Node | null} */ - let bestElement = currentElement; + let bestNode = currentNode; let score = 0; - while (currentElement) { - let newScore = scoreElement(currentElement, oldNode, ctx); + while (currentNode) { + let newScore = scoreElement(oldElement, currentNode, ctx); if (newScore > score) { - bestElement = currentElement; + bestNode = currentNode; score = newScore; } - currentElement = currentElement.nextSibling; + currentNode = currentNode.nextSibling; } - return bestElement; + return bestNode; } /** * - * @param {Node | null} node1 - * @param {Element} node2 + * @param {Element} element + * @param {Node | null} node * @param {MorphContext} ctx * @returns {number} */ // TODO: The function handles node1 and node2 as if they are Elements but the function is // called in places where node1 and node2 may be just Nodes, not Elements - function scoreElement(node1, node2, ctx) { - if (isSoftMatch(node2, node1)) { + function scoreElement(element, node, ctx) { + if (isSoftMatch(element, node)) { // ok to cast: isSoftMatch performs a null check return ( - 0.5 + getIdIntersectionCount(ctx, /** @type {Node} */ (node1), node2) + 0.5 + getIdIntersectionCount(ctx, element, /** @type {Node} */ (node)) ); } return 0; @@ -1180,79 +1190,58 @@ var Idiomorph = (function () { /** * - * @param {Node} tempNode + * @param {Node} node * @param {MorphContext} ctx */ - // TODO: The function handles tempNode as if it's Element but the function is called in - // places where tempNode may be just a Node, not an Element - function removeNode(tempNode, ctx) { - removeIdsFromConsideration(ctx, tempNode); - // skip remove callbacks when we're going to be restoring this from the pantry in the second pass - if ( - ctx.config.twoPass && - hasPersistentIdNodes(ctx, tempNode) && - tempNode instanceof Element - ) { - moveToPantry(tempNode, ctx); + function removeNode(node, ctx) { + removeIdsFromConsideration(ctx, node); + // skip remove callbacks when we're going to be restoring this from the pantry later + if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { + moveBefore(ctx.pantry, node, null); } else { - if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; - tempNode.parentNode?.removeChild(tempNode); - ctx.callbacks.afterNodeRemoved(tempNode); + if (ctx.callbacks.beforeNodeRemoved(node) === false) return; + node.parentNode?.removeChild(node); + ctx.callbacks.afterNodeRemoved(node); } } /** + * Search for an element by id within the document and pantry, and move it using moveBefore. * - * @param {Node} node + * @param {Element} parentNode - The parent node to which the element will be moved. + * @param {string} id - The ID of the element to be moved. + * @param {Node | null} after - The reference node to insert the element before. + * If `null`, the element is appended as the last child. * @param {MorphContext} ctx + * @returns {Element} The found element */ - function moveToPantry(node, ctx) { - if (ctx.callbacks.beforeNodePantried(node) === false) return; - - Array.from(node.childNodes).forEach((child) => { - moveToPantry(child, ctx); - }); - - // After processing children, process the current node - if (ctx.persistentIds.has(/** @type {Element} */ (node).id)) { - // @ts-ignore - use proposed moveBefore feature - if (ctx.pantry.moveBefore) { - // @ts-ignore - use proposed moveBefore feature - ctx.pantry.moveBefore(node, null); - } else { - ctx.pantry.insertBefore(node, null); - } - } else { - if (ctx.callbacks.beforeNodeRemoved(node) === false) return; - node.parentNode?.removeChild(node); - ctx.callbacks.afterNodeRemoved(node); - } + function moveBeforeById(parentNode, id, after, ctx) { + const target = + /** @type {Element} - will always be found */ + ( + ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`) + ); + moveBefore(parentNode, target, after); + return target; } /** + * Moves an element before another element within the same parent. + * Uses the proposed `moveBefore` API if available, otherwise falls back to `insertBefore`. + * This is essentialy a forward-compat wrapper. * - * @param {Node | null} root - * @param {MorphContext} ctx + * @param {Element} parentNode - The parent node containing the after element. + * @param {Node} element - The element to be moved. + * @param {Node | null} after - The reference node to insert `element` before. + * If `null`, `element` is appended as the last child. */ - function restoreFromPantry(root, ctx) { - if (root instanceof Element) { - Array.from(ctx.pantry.children) - .reverse() - .forEach((element) => { - const matchElement = root.querySelector(`#${element.id}`); - if (matchElement) { - // @ts-ignore - use proposed moveBefore feature - if (matchElement.parentElement?.moveBefore) { - // @ts-ignore - use proposed moveBefore feature - matchElement.parentElement.moveBefore(element, matchElement); - } else { - matchElement.before(element); - } - morphOldNodeTo(element, matchElement, ctx); - matchElement.remove(); - } - }); - ctx.pantry.remove(); + function moveBefore(parentNode, element, after) { + // @ts-ignore - use proposed moveBefore feature + if (parentNode.moveBefore) { + // @ts-ignore - use proposed moveBefore feature + parentNode.moveBefore(element, after); + } else { + parentNode.insertBefore(element, after); } } @@ -1334,12 +1323,12 @@ var Idiomorph = (function () { * @param {Element} content * @returns {Element[]} */ - function nodesWithIds(content) { - let nodes = Array.from(content.querySelectorAll("[id]")); + function elementsWithIds(content) { + let elements = Array.from(content.querySelectorAll("[id]")); if (content.id) { - nodes.push(content); + elements.push(content); } - return nodes; + return elements; } /** @@ -1352,7 +1341,7 @@ var Idiomorph = (function () { */ function populateIdMapForNode(node, idMap) { let nodeParent = node.parentElement; - for (const elt of nodesWithIds(node)) { + for (const elt of elementsWithIds(node)) { /** * @type {Element|null} */ @@ -1399,14 +1388,31 @@ var Idiomorph = (function () { * @returns {Set} the id set of all persistent nodes that exist in both old and new content */ function createPersistentIds(oldContent, newContent) { - const toIdTagName = (/** @type {Element} */ node) => - node.tagName + "#" + node.id; - const oldIdSet = new Set(nodesWithIds(oldContent).map(toIdTagName)); - + let oldIdMap = new Map(); + let dupSet = new Set(); + for (const oldNode of elementsWithIds(oldContent)) { + const id = oldNode.id; + // if already in map then log duplicates to be skipped + if (oldIdMap.get(id)) { + dupSet.add(id); + } else { + oldIdMap.set(id, oldNode.tagName); + } + } let matchIdSet = new Set(); - for (const newNode of nodesWithIds(newContent)) { - if (oldIdSet.has(toIdTagName(newNode))) { - matchIdSet.add(newNode.id); + for (const newNode of elementsWithIds(newContent)) { + const id = newNode.id; + const oldTagName = oldIdMap.get(id); + // if already matched skip id as duplicate but also skip if tag types mismatch because it could match later + if ( + matchIdSet.has(id) || + (oldTagName && oldTagName !== newNode.tagName) + ) { + dupSet.add(id); + matchIdSet.delete(id); + } + if (oldTagName === newNode.tagName && !dupSet.has(id)) { + matchIdSet.add(id); } } return matchIdSet; diff --git a/test/bootstrap.js b/test/bootstrap.js index b08dbef..174c4ae 100644 --- a/test/bootstrap.js +++ b/test/bootstrap.js @@ -35,25 +35,44 @@ describe("Bootstrap test", function () { it("basic deep morph works", function (done) { let div1 = make( - '
A
B
C
', + ` +
+
+
A
+
+
+
B
+
+
+
C
+
+
`.trim(), ); let d1 = div1.querySelector("#d1"); let d2 = div1.querySelector("#d2"); let d3 = div1.querySelector("#d3"); - let morphTo = - '
E
F
D
'; + let morphTo = ` +
+
+
E
+
+
+
F
+
+
+
D
+
+
`.trim(); let div2 = make(morphTo); print(div1); Idiomorph.morph(div1, div2); print(div1); - // first paragraph should have been discarded in favor of later matches - d1.innerHTML.should.not.equal("D"); - - // second and third paragraph should have morphed + // all three paragraphs should have been morphed + d1.innerHTML.should.equal("D"); d2.innerHTML.should.equal("E"); d3.innerHTML.should.equal("F"); diff --git a/test/index.html b/test/index.html index 9a735d2..29f5c08 100644 --- a/test/index.html +++ b/test/index.html @@ -44,7 +44,7 @@

Mocha Test Suite

- +
diff --git a/test/two-pass.js b/test/retain-hidden-state.js similarity index 58% rename from test/two-pass.js rename to test/retain-hidden-state.js index f702a4e..4a50a97 100644 --- a/test/two-pass.js +++ b/test/retain-hidden-state.js @@ -1,7 +1,7 @@ -describe("Two-pass option for retaining more state", function () { +describe("algorithm", function () { setup(); - it("fails to preserve all non-attribute element state with single-pass option", function () { + it("preserves all non-attribute element state", function () { getWorkArea().append( make(`
@@ -21,37 +21,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: false, - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - const states = Array.from(getWorkArea().querySelectorAll("input")).map( - (e) => e.indeterminate, - ); - states.should.eql([true, false]); - }); - - it("preserves all non-attribute element state with two-pass option", function () { - getWorkArea().append( - make(` -
- - -
- `), - ); - document.getElementById("first").indeterminate = true; - document.getElementById("second").indeterminate = true; - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -61,7 +30,7 @@ describe("Two-pass option for retaining more state", function () { states.should.eql([true, true]); }); - it("preserves all non-attribute element state with two-pass option and outerHTML morphStyle", function () { + it("preserves all non-attribute element state and outerHTML morphStyle", function () { const div = make(`
@@ -78,7 +47,7 @@ describe("Two-pass option for retaining more state", function () {
`; - Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML", twoPass: true }); + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); getWorkArea().innerHTML.should.equal(finalSrc); const states = Array.from(getWorkArea().querySelectorAll("input")).map( @@ -109,7 +78,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -147,7 +115,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -185,7 +152,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -195,7 +161,7 @@ describe("Two-pass option for retaining more state", function () { states.should.eql([true, true]); }); - it("preserves focus state with two-pass option and outerHTML morphStyle", function () { + it("preserves focus state and outerHTML morphStyle", function () { const div = make(`
@@ -211,18 +177,41 @@ describe("Two-pass option for retaining more state", function () {
`; - Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML", twoPass: true }); + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + document.activeElement.outerHTML.should.equal( + document.getElementById("first").outerHTML, + ); + }); + + it("preserves focus state when previous element is replaced", function () { + getWorkArea().innerHTML = ` +
+ + +
+ `; + document.getElementById("focus").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); getWorkArea().innerHTML.should.equal(finalSrc); if (document.body.moveBefore) { document.activeElement.outerHTML.should.equal( - document.getElementById("first").outerHTML, + document.getElementById("focus").outerHTML, ); } else { document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log( - "preserves focus state with two-pass option and outerHTML morphStyle test needs moveBefore enabled to work properly", - ); + console.log("needs moveBefore enabled to work properly"); } }); @@ -247,7 +236,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -263,7 +251,39 @@ describe("Two-pass option for retaining more state", function () { } }); - it("preserves focus state when elements are moved between different containers", function () { + it("preserves focus state when focused element is moved between anonymous containers", function () { + getWorkArea().innerHTML = ` +
+ +
+
+ +
+ `; + document.getElementById("second").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("second").outerHTML, + ); + } else { + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + console.log("needs moveBefore enabled to work properly"); + } + }); + + it("preserves focus state when elements are moved between IDed containers", function () { getWorkArea().append( make(`
@@ -290,7 +310,6 @@ describe("Two-pass option for retaining more state", function () { `; Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML", - twoPass: true, }); getWorkArea().innerHTML.should.equal(finalSrc); @@ -301,52 +320,44 @@ describe("Two-pass option for retaining more state", function () { } else { document.activeElement.outerHTML.should.equal(document.body.outerHTML); console.log( - "preserves focus state when elements are moved between different containers test needs moveBefore enabled to work properly", + "preserves focus state when elements are moved between IDed containers test needs moveBefore enabled to work properly", ); } }); - it("preserves focus state when parents are reorderd", function () { + it("preserves focus state when parents are reordered", function () { getWorkArea().append( make(`
-
- +
+
- `), ); - document.getElementById("first").focus(); + document.getElementById("focus").focus(); let finalSrc = `
- `); }); + + it("duplicate ids on elements aborts matching to avoid invalid morph state", function () { + // we try to reuse existing ids where possible and has to exclude matching on duplicate ids + // to avoid losing content + getWorkArea().append( + make(` +
+
+ + +
+ +
+ `), + ); + document.getElementById("first").focus(); + + let finalSrc = ` +
+
+ + +
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + // should have lost active element focus because duplicate ids can not be processed properly + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + }); + + it("duplicate destination ids on elements aborts matching to avoid invalid morph state", function () { + // exclude matching on duplicate ids to avoid losing content + getWorkArea().append( + make(` +
+
+ +
+ +
+ `), + ); + document.getElementById("first").focus(); + + let finalSrc = ` +
+
+ + +
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + // should have lost active element focus because duplicate ids can not be processed properly + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + }); + + it("preserves all non-attribute element state and outerHTML morphStyle when morphing to two top level nodes", function () { + // when using outerHTML you can replace one node with two nodes with the state preserving items split and it will just + // pick one best node to morph and just insert the other nodes so need to check these also retain state + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + document.getElementById("first").indeterminate = true; + document.getElementById("second").indeterminate = true; + + let finalSrc = ` +
+ +
+ + `; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + const states = Array.from(getWorkArea().querySelectorAll("input")).map( + (e) => e.indeterminate, + ); + states.should.eql([true, true]); + }); + + it("preserves all non-attribute element state and outerHTML morphStyle when morphing to two top level nodes with nesting", function () { + // when using outerHTML you can replace one node with two nodes with the state preserving items split and it will just + // pick one best node to morph and just insert the other nodes so need to check these also retain state + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + document.getElementById("first").indeterminate = true; + document.getElementById("second").indeterminate = true; + + let finalSrc = ` +
+ +
+
+ +
+ `; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + const states = Array.from(getWorkArea().querySelectorAll("input")).map( + (e) => e.indeterminate, + ); + states.should.eql([true, true]); + }); + + it("preserves all non-attribute element state when wrapping element changes tag", function () { + // just changing the type from div to span of the wrapper causes softmatch to fail so it abandons all hope + // of morphing and just inserts the node so we need to check this still handles preserving state here. + const div = make(` +
+
+
+ `); + getWorkArea().append(div); + document.getElementById("first").indeterminate = true; + + let finalSrc = ` +
+ +
+ `; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + const states = Array.from(getWorkArea().querySelectorAll("input")).map( + (e) => e.indeterminate, + ); + states.should.eql([true]); + }); + + it("preserves all non-attribute element state when wrapping element changes tag at top level", function () { + // just changing the type from div to span of the top wrapping item with outerHTML morph will cause morphOldNodeTo function + // to morph the two nodes that don't softmatch that also needs to handle preserving state + const div = make(`
`); + getWorkArea().append(div); + document.getElementById("first").indeterminate = true; + + let finalSrc = ``; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + const states = Array.from(getWorkArea().querySelectorAll("input")).map( + (e) => e.indeterminate, + ); + states.should.eql([true]); + }); }); diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index d695702..3f7231b 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -22,8 +22,6 @@ let config = { - - From 2818a300db8d73ec40417929e7ff044fd89b50f3 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 10 Jan 2025 12:49:16 +1300 Subject: [PATCH 02/50] improve morphChildren insertion point null checking --- src/idiomorph.js | 96 ++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index ec190bc..b0e14d5 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -394,68 +394,62 @@ var Idiomorph = (function () { // run through all the new content for (const newChild of newParent.childNodes) { - // if the current node has an id set match then morph - if (isIdSetMatch(newChild, insertionPoint, ctx)) { - insertionPoint = morphChild( - /** @type {Node} */ (insertionPoint), - newChild, - insertionPoint, - ctx, - ); - continue; - } - - // otherwise search forward in the existing old children for an id set match - const idSetMatch = findIdSetMatch( - oldParent, - newParent, - newChild, - insertionPoint, - ctx, - ); - if (idSetMatch) { - insertionPoint = morphChild(idSetMatch, newChild, insertionPoint, ctx); - continue; - } + // if we have reached the end of the old parent insertionPoint will be null so skip to end and insert + if (insertionPoint != null) { + // if the current node has an id set match then morph + if (isIdSetMatch(newChild, insertionPoint, ctx)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } - // if the current point is already a soft match morph - if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( - /** @type {Node} */ (insertionPoint), + // otherwise search forward in the existing old children for an id set match + const idSetMatch = findIdSetMatch( + oldParent, + newParent, newChild, insertionPoint, ctx, ); - continue; - } + if (idSetMatch) { + insertionPoint = morphChild( + idSetMatch, + newChild, + insertionPoint, + ctx, + ); + continue; + } - // search forward in the existing old children for a soft match for the current node - const softMatch = findSoftMatch( - oldParent, - newParent, - newChild, - insertionPoint, - ctx, - ); - if (softMatch) { - insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); - continue; - } + // if the current point is already a soft match morph + if (isSoftMatch(insertionPoint, newChild)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } - // match is somewhere else in the document, find it and move it - if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { - const movedChild = moveBeforeById( + // search forward in the existing old children for a soft match for the current node + const softMatch = findSoftMatch( oldParent, - /** @type {Element} */ (newChild).id, + newParent, + newChild, insertionPoint, ctx, ); - morphOldNodeTo(movedChild, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); - continue; + if (softMatch) { + insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); + continue; + } } - - // last resort, create a new node from scratch + // last resort, insert a new node from scratch or reuse and morph a remote node with matching id insertOrMorphNode(oldParent, newChild, insertionPoint, ctx); } @@ -906,8 +900,6 @@ var Idiomorph = (function () { insertionPoint, ctx, ) { - if (!insertionPoint) return null; - // max id matches we are willing to discard in our search let newChildPotentialIdCount = getIdIntersectionCount( ctx, From 49c5b6b1c9c4bd547c101c767e20b2707a441577 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 10 Jan 2025 16:32:58 +1300 Subject: [PATCH 03/50] Add back findIdSetMatch test and handle moveBefore failure --- src/idiomorph.js | 9 +++++++-- test/retain-hidden-state.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index b0e14d5..00cfb05 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -1230,8 +1230,13 @@ var Idiomorph = (function () { function moveBefore(parentNode, element, after) { // @ts-ignore - use proposed moveBefore feature if (parentNode.moveBefore) { - // @ts-ignore - use proposed moveBefore feature - parentNode.moveBefore(element, after); + try { + // @ts-ignore - use proposed moveBefore feature + parentNode.moveBefore(element, after); + } catch (e) { + // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry + parentNode.insertBefore(element, after); + } } else { parentNode.insertBefore(element, after); } diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index 4a50a97..1339c5d 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -653,4 +653,38 @@ describe("algorithm", function () { ); states.should.eql([true]); }); + + it("findIdSetMatch rejects morphing node that would lose more IDs", function () { + const div = make(` +
+ + + +
+ `); + getWorkArea().append(div); + document.getElementById("third").focus(); + + let finalSrc = ` +
+ + + +
+ `; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + // moveBefore would prevent the node being discarded and losing state so we can't detect easily if findIdSetMatch rejected the morphing + document.activeElement.outerHTML.should.equal( + document.getElementById("third").outerHTML, + ); + } else { + // but testing with no moveBefore we can test it + // third paragrah should have been discarded because it was moved in front of two other paragraphs with ID's + // it should detect that removing the first two nodes with ID's to preserve just one ID is not worth it + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + } + }); }); From 243749fdf5a6b87548c7bc5f2738cdfb1340a4d4 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 10 Jan 2025 20:20:45 +1300 Subject: [PATCH 04/50] Improve idMap to only include persistent Ids --- src/idiomorph.js | 171 ++++++++++++++++++++++++----------------------- 1 file changed, 86 insertions(+), 85 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 00cfb05..37ee07a 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -72,6 +72,12 @@ * @property {ConfigHeadInternal} head */ +/** + * @typedef {Object} IdSets + * @property {Set} persistentIds + * @property {Map>} idMap + */ + /** * @typedef {Function} Morph * @@ -244,10 +250,6 @@ var Idiomorph = (function () { * @param {MorphContext} ctx * @returns {boolean} */ - // TODO: ignoreActive and ignoreActiveValue are marked as optional since they are not - // initialised in the default config object. As a result the && in the function body may - // return undefined instead of boolean. Either expand the type of the return value to - // include undefined or wrap the ctx.ignoreActiveValue into a Boolean() function ignoreValueOfActiveElement(possibleActiveElement, ctx) { return ( !!ctx.ignoreActiveValue && @@ -304,6 +306,8 @@ var Idiomorph = (function () { removeNode(oldNode, ctx); return null; } else if (!isSoftMatch(oldNode, newContent)) { + // A parent node that has children with persistent ids may need to be morphed to a new type + // we can't do this so instead insert a dummy node and morph it before removing the old node insertOrMorphNode(oldNode.parentElement, newContent, oldNode, ctx); const newNode = oldNode.previousSibling; removeNode(oldNode, ctx); @@ -806,6 +810,7 @@ var Idiomorph = (function () { */ function createMorphContext(oldNode, newContent, config) { const mergedConfig = mergeDefaults(config); + const { persistentIds, idMap } = createIdMaps(oldNode, newContent); return { target: oldNode, newContent: newContent, @@ -813,9 +818,9 @@ var Idiomorph = (function () { morphStyle: mergedConfig.morphStyle, ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, - idMap: createIdMap(oldNode, newContent), + idMap: idMap, deadIds: new Set(), - persistentIds: createPersistentIds(oldNode, newContent), + persistentIds: persistentIds, pantry: createPantry(), callbacks: mergedConfig.callbacks, head: mergedConfig.head, @@ -836,8 +841,6 @@ var Idiomorph = (function () { * @param {MorphContext} ctx * @returns {boolean} */ - // TODO: The function handles this as if it's Element or null, but the function is called in - // places where the arguments may be just a Node, not an Element function isIdSetMatch(node1, node2, ctx) { if ( node1 instanceof Element && @@ -901,22 +904,15 @@ var Idiomorph = (function () { ctx, ) { // max id matches we are willing to discard in our search - let newChildPotentialIdCount = getIdIntersectionCount( - ctx, - newChild, - oldParent, - ); + let newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); /** * @type {Node | null} */ - let potentialMatch = null; // only search forward if there is a possibility of an id match if (newChildPotentialIdCount > 0) { - // TODO: This is ghosting the potentialMatch variable outside of this block. - // Probably an error - potentialMatch = insertionPoint; + let potentialMatch = insertionPoint; // if there is a possibility of an id match, scan forward // keep track of the potential id match count we are discarding (the // newChildPotentialIdCount must be greater than this to make it likely @@ -929,11 +925,8 @@ var Idiomorph = (function () { } // computer the other potential matches of this new content - otherMatchCount += getIdIntersectionCount( - ctx, - potentialMatch, - newContent, - ); + otherMatchCount += getPersistentIdNodeCount(ctx, potentialMatch); + if (otherMatchCount > newChildPotentialIdCount) { // if we have more potential id matches in _other_ content, we // do not have a good candidate for an id match, so return null @@ -944,7 +937,7 @@ var Idiomorph = (function () { potentialMatch = potentialMatch.nextSibling; } } - return potentialMatch; + return null; } /** @@ -973,7 +966,7 @@ var Idiomorph = (function () { let siblingSoftMatchCount = 0; while (potentialSoftMatch != null) { - if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { + if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { // the current potential soft match has a potential id set match with the remaining new // content so bail out of looking return null; @@ -1164,18 +1157,13 @@ var Idiomorph = (function () { /** * * @param {Element} element - * @param {Node | null} node + * @param {Node} node * @param {MorphContext} ctx * @returns {number} */ - // TODO: The function handles node1 and node2 as if they are Elements but the function is - // called in places where node1 and node2 may be just Nodes, not Elements function scoreElement(element, node, ctx) { if (isSoftMatch(element, node)) { - // ok to cast: isSoftMatch performs a null check - return ( - 0.5 + getIdIntersectionCount(ctx, element, /** @type {Node} */ (node)) - ); + return 0.5 + getPersistentIdNodeCount(ctx, node); } return 0; } @@ -1281,6 +1269,17 @@ var Idiomorph = (function () { } } + /** + * + * @param {MorphContext} ctx + * @param {Node} node + * @returns {number} + */ + function getPersistentIdNodeCount(ctx, node) { + let idSet = ctx.idMap.get(node) || EMPTY_SET; + return idSet.size; + } + /** * * @param {MorphContext} ctx @@ -1288,12 +1287,7 @@ var Idiomorph = (function () { * @returns {boolean} */ function hasPersistentIdNodes(ctx, node) { - for (const id of ctx.idMap.get(node) || EMPTY_SET) { - if (ctx.persistentIds.has(id)) { - return true; - } - } - return false; + return getPersistentIdNodeCount(ctx, node) > 0; } /** @@ -1333,27 +1327,30 @@ var Idiomorph = (function () { * argument and populates id sets for those nodes and all their parents, generating * a set of ids contained within all nodes for the entire hierarchy in the DOM * - * @param {Element} node + * @param {Element|null} nodeParent + * @param {Element[]} nodes + * @param {Set} persistentIds * @param {Map>} idMap */ - function populateIdMapForNode(node, idMap) { - let nodeParent = node.parentElement; - for (const elt of elementsWithIds(node)) { - /** - * @type {Element|null} - */ - let current = elt; - // walk up the parent hierarchy of that element, adding the id - // of element to the parent's id set - while (current !== nodeParent && current != null) { - let idSet = idMap.get(current); - // if the id set doesn't exist, create it and insert it in the map - if (idSet == null) { - idSet = new Set(); - idMap.set(current, idSet); + function populateIdMapForNode(nodeParent, nodes, persistentIds, idMap) { + for (const elt of nodes) { + if (persistentIds.has(elt.id)) { + /** + * @type {Element|null} + */ + let current = elt; + // walk up the parent hierarchy of that element, adding the id + // of element to the parent's id set + while (current !== nodeParent && current != null) { + let idSet = idMap.get(current); + // if the id set doesn't exist, create it and insert it in the map + if (idSet == null) { + idSet = new Set(); + idMap.set(current, idSet); + } + idSet.add(elt.id); + current = current.parentElement; } - idSet.add(elt.id); - current = current.parentElement; } } } @@ -1366,53 +1363,57 @@ var Idiomorph = (function () { * * @param {Element} oldContent the old content that will be morphed * @param {Element} newContent the new content to morph to - * @returns {Map>} a map of nodes to id sets for the + * @returns {IdSets} a map of nodes to id sets for the */ - function createIdMap(oldContent, newContent) { - /** - * - * @type {Map>} - */ - let idMap = new Map(); - populateIdMapForNode(oldContent, idMap); - populateIdMapForNode(newContent, idMap); - return idMap; - } - - /** - * @param {Element} oldContent the old content that will be morphed - * @param {Element} newContent the new content to morph to - * @returns {Set} the id set of all persistent nodes that exist in both old and new content - */ - function createPersistentIds(oldContent, newContent) { + function createIdMaps(oldContent, newContent) { + // Calculate ids that persist between the two contents exculuding duplicates first let oldIdMap = new Map(); let dupSet = new Set(); - for (const oldNode of elementsWithIds(oldContent)) { - const id = oldNode.id; + const oldElts = elementsWithIds(oldContent); + for (const oldElt of oldElts) { + const id = oldElt.id; // if already in map then log duplicates to be skipped if (oldIdMap.get(id)) { dupSet.add(id); } else { - oldIdMap.set(id, oldNode.tagName); + oldIdMap.set(id, oldElt.tagName); } } - let matchIdSet = new Set(); - for (const newNode of elementsWithIds(newContent)) { - const id = newNode.id; + let persistentIds = new Set(); + const newElts = elementsWithIds(newContent); + for (const newElt of newElts) { + const id = newElt.id; const oldTagName = oldIdMap.get(id); // if already matched skip id as duplicate but also skip if tag types mismatch because it could match later if ( - matchIdSet.has(id) || - (oldTagName && oldTagName !== newNode.tagName) + persistentIds.has(id) || + (oldTagName && oldTagName !== newElt.tagName) ) { dupSet.add(id); - matchIdSet.delete(id); + persistentIds.delete(id); } - if (oldTagName === newNode.tagName && !dupSet.has(id)) { - matchIdSet.add(id); + if (oldTagName === newElt.tagName && !dupSet.has(id)) { + persistentIds.add(id); } } - return matchIdSet; + /** + * + * @type {Map>} + */ + let idMap = new Map(); + populateIdMapForNode( + oldContent.parentElement, + newElts, + persistentIds, + idMap, + ); + populateIdMapForNode( + newContent.parentElement, + oldElts, + persistentIds, + idMap, + ); + return { persistentIds, idMap }; } //============================================================================= From e589874832a074a4dde4f00824b771c13d44508b Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sat, 11 Jan 2025 02:53:14 +1300 Subject: [PATCH 05/50] Add findSoftMatch and moveBefore loc tests --- test/core.js | 12 ++++++ test/retain-hidden-state.js | 76 +++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/test/core.js b/test/core.js index d5326cc..8c1c98c 100644 --- a/test/core.js +++ b/test/core.js @@ -519,4 +519,16 @@ describe("Core morphing tests", function () { Idiomorph.defaults.morphStyle = "outerHTML"; } }); + + it("add loc coverage for findSoftMatch aborting on two future soft matches", function () { + // when nodes can't be softMatched because they have different types it will scan ahead + // but it aborts the scan ahead if it finds two nodes ahead in both the new and old content + // that softmatch so it can just insert the mis matched node it is on and get to the matching. + // had no test coverage but not easy to test but at least it is called now. + let initial = parseHTML("

"); + let finalSrc = "

"; + let final = parseHTML(finalSrc); + Idiomorph.morph(initial.body, final.body); + initial.body.outerHTML.should.equal(finalSrc); + }); }); diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index 1339c5d..b15706e 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -687,4 +687,80 @@ describe("algorithm", function () { document.activeElement.outerHTML.should.equal(document.body.outerHTML); } }); + + it("check moveBefore function fails back to insertBefore if moveBefore fails", function () { + getWorkArea().append( + make(` +
+ + +
+ `), + ); + // replace moveBefore function with a boolean which will fail the try catch + document.getElementById("first").parentNode.moveBefore = true; + document.getElementById("second").parentNode.moveBefore = true; + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + }); + + it("check moveBefore function falls back to insertBefore if moveBefore fails", function () { + getWorkArea().innerHTML = ` +
+ + +
+ `; + document.getElementById("focus").focus(); + // break the moveBefore function so it fails back to insertBefore and focus is not preserved + document.getElementById("focus").parentNode.moveBefore = true; + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + }); + + it("check moveBefore function falls back to insertBefore if moveBefore is missing", function () { + getWorkArea().innerHTML = ` +
+ + +
+ `; + document.getElementById("focus").focus(); + // remove the moveBefore function so it fails back to insertBefore and focus is not preserved + document.getElementById("focus").parentNode.moveBefore = undefined; + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + }); }); From a50ee6e5b4333260db05aea80c459a74db614426 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Fri, 10 Jan 2025 12:27:36 -0600 Subject: [PATCH 06/50] replace bespoke outerHTML morphing algorithm with our core one. --- src/idiomorph.js | 193 +++++++++------------------------------------- test/bootstrap.js | 29 ------- test/core.js | 20 ++++- 3 files changed, 55 insertions(+), 187 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 37ee07a..ccd1cfd 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -212,34 +212,10 @@ var Idiomorph = (function () { ctx.pantry.remove(); return Array.from(oldNode.children); } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { - // otherwise find the best element match in the new content, morph that, and merge its siblings - // into either side of the best match - let bestMatch = findBestNodeMatch(oldNode, normalizedNewContent, ctx); - - // stash the siblings that will need to be inserted on either side of the best match - let previousSibling = bestMatch?.previousSibling ?? null; - let nextSibling = bestMatch?.nextSibling ?? null; - - // morph it - let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); - - if (bestMatch) { - // if there was a best match, merge the siblings in too and return the - // whole bunch - if (morphedNode) { - const elements = insertSiblings( - previousSibling, - morphedNode, - nextSibling, - ctx, - ); - ctx.pantry.remove(); - return elements; - } - } else { - // otherwise nothing was added to the DOM - return []; - } + const normalizedOldNode = normalizeContent(oldNode); + morphChildren(normalizedOldNode, normalizedNewContent, ctx); + ctx.pantry.remove(); + return Array.from(normalizedOldNode.children); } else { throw "Do not understand how to morph style " + ctx.morphStyle; } @@ -295,50 +271,41 @@ var Idiomorph = (function () { /** * @param {Node} oldNode root node to merge content into - * @param {Node | null} newContent new content to merge + * @param {Node} newContent new content to merge * @param {MorphContext} ctx the merge context * @returns {Node | null} the element that ended up in the DOM */ function morphOldNodeTo(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) { // don't morph focused element - } else if (newContent == null) { - removeNode(oldNode, ctx); return null; - } else if (!isSoftMatch(oldNode, newContent)) { - // A parent node that has children with persistent ids may need to be morphed to a new type - // we can't do this so instead insert a dummy node and morph it before removing the old node - insertOrMorphNode(oldNode.parentElement, newContent, oldNode, ctx); - const newNode = oldNode.previousSibling; - removeNode(oldNode, ctx); - return newNode; + } + + if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) { + return oldNode; + } + + if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { + // ignore the head element + } else if ( + oldNode instanceof HTMLHeadElement && + ctx.head.style !== "morph" + ) { + // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above + handleHeadElement( + oldNode, + /** @type {HTMLHeadElement} */ (newContent), + ctx, + ); } else { - if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) - return oldNode; - - if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { - // ignore the head element - } else if ( - oldNode instanceof HTMLHeadElement && - ctx.head.style !== "morph" - ) { - // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above - handleHeadElement( - oldNode, - /** @type {HTMLHeadElement} */ (newContent), - ctx, - ); - } else { - syncNode(oldNode, newContent, ctx); - if (!ignoreValueOfActiveElement(oldNode, ctx)) { - // @ts-ignore newContent can be a node here because .firstChild will be null - morphChildren(oldNode, newContent, ctx); - } + syncNode(oldNode, newContent, ctx); + if (!ignoreValueOfActiveElement(oldNode, ctx)) { + // @ts-ignore newContent can be a node here because .firstChild will be null + morphChildren(oldNode, newContent, ctx); } - ctx.callbacks.afterNodeMorphed(oldNode, newContent); - return oldNode; } - return null; + ctx.callbacks.afterNodeMorphed(oldNode, newContent); + return oldNode; } /** @@ -1063,10 +1030,14 @@ var Idiomorph = (function () { // the template tag created by idiomorph parsing can serve as a dummy parent return /** @type {Element} */ (newContent); } else if (newContent instanceof Node) { - // a single node is added as a child to a dummy parent - const dummyParent = document.createElement("div"); - dummyParent.append(newContent); - return dummyParent; + if (newContent.parentNode) { + return /** @type {Element} */ (newContent.parentNode); + } else { + // a single node is added as a child to a dummy parent + const dummyParent = document.createElement("div"); + dummyParent.append(newContent); + return dummyParent; + } } else { // all nodes in the array or HTMLElement collection are consolidated under // a single dummy parent element @@ -1078,96 +1049,6 @@ var Idiomorph = (function () { } } - /** - * - * @param {Node | null} previousSibling - * @param {Node} morphedNode - * @param {Node | null} nextSibling - * @param {MorphContext} ctx - * @returns {Node[]} - */ - function insertSiblings(previousSibling, morphedNode, nextSibling, ctx) { - /** - * @type {Node[]} - */ - let stack = []; - /** - * @type {Node[]} - */ - let added = []; - while (previousSibling != null) { - stack.push(previousSibling); - previousSibling = previousSibling.previousSibling; - } - // Base the loop on the node variable, so that you do not need runtime checks for - // undefined value inside the loop - let node = stack.pop(); - while (node !== undefined) { - added.push(node); // push added preceding siblings on in order and insert - insertOrMorphNode(morphedNode.parentElement, node, morphedNode, ctx); - node = stack.pop(); - } - added.push(morphedNode); - while (nextSibling != null) { - stack.push(nextSibling); - added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add - nextSibling = nextSibling.nextSibling; - } - while (stack.length > 0) { - const node = /** @type {Node} */ (stack.pop()); - insertOrMorphNode( - morphedNode.parentElement, - node, - morphedNode.nextSibling, - ctx, - ); - } - return added; - } - - /** - * - * @param {Element} oldElement - * @param {Element} newContent - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function findBestNodeMatch(oldElement, newContent, ctx) { - /** - * @type {Node | null} - */ - let currentNode; - currentNode = newContent.firstChild; - /** - * @type {Node | null} - */ - let bestNode = currentNode; - let score = 0; - while (currentNode) { - let newScore = scoreElement(oldElement, currentNode, ctx); - if (newScore > score) { - bestNode = currentNode; - score = newScore; - } - currentNode = currentNode.nextSibling; - } - return bestNode; - } - - /** - * - * @param {Element} element - * @param {Node} node - * @param {MorphContext} ctx - * @returns {number} - */ - function scoreElement(element, node, ctx) { - if (isSoftMatch(element, node)) { - return 0.5 + getPersistentIdNodeCount(ctx, node); - } - return 0; - } - /** * * @param {Node} node diff --git a/test/bootstrap.js b/test/bootstrap.js index 174c4ae..6033615 100644 --- a/test/bootstrap.js +++ b/test/bootstrap.js @@ -110,33 +110,4 @@ describe("Bootstrap test", function () { // }, 0) // print(div1); }); - - it("findIdSetMatch rejects morphing node that would lose more IDs", function (done) { - let div1 = make( - '
A
B
C
', - ); - - let d1 = div1.querySelector("#d1"); - let d2 = div1.querySelector("#d2"); - let d3 = div1.querySelector("#d3"); - - let morphTo = - '
F
D
E
'; - let div2 = make(morphTo); - - print(div1); - Idiomorph.morph(div1, div2); - print(div1); - - // first and second paragraph should have morphed - d1.innerHTML.should.equal("D"); - d2.innerHTML.should.equal("E"); - - // third paragrah should have been discarded because it was moved in front of two other paragraphs with ID's - // it should detect that removing the first two nodes with ID's to preserve just one ID is not worth it - d3.innerHTML.should.not.equal("F"); - - div1.outerHTML.should.equal(morphTo); - done(); - }); }); diff --git a/test/core.js b/test/core.js index 8c1c98c..d13459c 100644 --- a/test/core.js +++ b/test/core.js @@ -7,7 +7,7 @@ describe("Core morphing tests", function () { initial.isConnected.should.equal(false); }); - it("morphs outerHTML as content properly when argument is single node", function () { + it("morphs outerHTML as content properly when argument is single node string", function () { let initial = make(""); let finalSrc = ""; let final = make(finalSrc); @@ -15,6 +15,14 @@ describe("Core morphing tests", function () { initial.outerHTML.should.equal(""); }); + it("morphs outerHTML as content properly when argument is single node", function () { + let initial = make(""); + let element = document.createElement("button"); + element.innerText = "Bar"; + Idiomorph.morph(initial, element, { morphStyle: "outerHTML" }); + initial.outerHTML.should.equal(""); + }); + it("morphs outerHTML as content properly when argument is string", function () { let initial = make(""); let finalSrc = ""; @@ -105,7 +113,7 @@ describe("Core morphing tests", function () { initial.outerHTML.should.equal("
"); }); - it("morphs innerHTML as content properly when argument is single node", function () { + it("morphs innerHTML as content properly when argument is single node string", function () { let initial = make("
Foo
"); let finalSrc = ""; let final = make(finalSrc); @@ -113,6 +121,14 @@ describe("Core morphing tests", function () { initial.outerHTML.should.equal("
"); }); + it("morphs innerHTML as content properly when argument is single node", function () { + let initial = make("
Foo
"); + let element = document.createElement("button"); + element.innerText = "Bar"; + Idiomorph.morph(initial, element, { morphStyle: "innerHTML" }); + initial.outerHTML.should.equal("
"); + }); + it("morphs innerHTML as content properly when argument is string", function () { let initial = make(""); let finalSrc = ""; From 9fa448fdcc675c493f65bf458888b08f156f0f94 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sun, 12 Jan 2025 03:06:18 +1300 Subject: [PATCH 07/50] implement bestMatch feature in morphChildren and tidy up idSets --- src/idiomorph.js | 224 +++++++++++++++++++++++++---------------------- 1 file changed, 118 insertions(+), 106 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index ccd1cfd..17d6f51 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -106,7 +106,6 @@ var Idiomorph = (function () { * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue * @property {Map>} idMap * @property {Set} persistentIds - * @property {Set} deadIds * @property {ConfigInternal['callbacks']} callbacks * @property {ConfigInternal['head']} head * @property {HTMLDivElement} pantry @@ -205,20 +204,17 @@ var Idiomorph = (function () { return; } } - + let normalizedOldNode; if (ctx.morphStyle === "innerHTML") { - // innerHTML, so we are only updating the children - morphChildren(oldNode, normalizedNewContent, ctx); - ctx.pantry.remove(); - return Array.from(oldNode.children); + normalizedOldNode = oldNode; } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { - const normalizedOldNode = normalizeContent(oldNode); - morphChildren(normalizedOldNode, normalizedNewContent, ctx); - ctx.pantry.remove(); - return Array.from(normalizedOldNode.children); + normalizedOldNode = normalizeContent(oldNode); } else { throw "Do not understand how to morph style " + ctx.morphStyle; } + morphChildren(normalizedOldNode, normalizedNewContent, ctx); + ctx.pantry.remove(); + return Array.from(normalizedOldNode.children); } /** @@ -266,7 +262,6 @@ var Idiomorph = (function () { ctx.callbacks.afterNodeAdded(newClonedChild); } } - removeIdsFromConsideration(ctx, newChild); } /** @@ -322,7 +317,6 @@ var Idiomorph = (function () { moveBefore(oldNode.parentElement, oldNode, insertionPoint); } morphOldNodeTo(oldNode, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); return oldNode.nextSibling; } @@ -362,62 +356,71 @@ var Idiomorph = (function () { } let insertionPoint = /** @type {Node | null} */ (oldParent.firstChild); + let bestMatch = /** @type {Node | null} */ (null); // run through all the new content for (const newChild of newParent.childNodes) { // if we have reached the end of the old parent insertionPoint will be null so skip to end and insert if (insertionPoint != null) { - // if the current node has an id set match then morph - if (isIdSetMatch(newChild, insertionPoint, ctx)) { - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - continue; + // if last remaining child node then make sure we morph with the best remaining node if there are multiple + if (!insertionPoint.nextSibling && newChild.nextSibling) { + bestMatch = findBestNodeMatch(insertionPoint, newChild, ctx); } - // otherwise search forward in the existing old children for an id set match - const idSetMatch = findIdSetMatch( - oldParent, - newParent, - newChild, - insertionPoint, - ctx, - ); - if (idSetMatch) { - insertionPoint = morphChild( - idSetMatch, + // if there is no bestMatch or we have found the bestMatch then morph, else skip to end and insert + if (!bestMatch || bestMatch === newChild) { + // if the current node has an id set match then morph + if (isIdSetMatch(insertionPoint, newChild, ctx)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // otherwise search forward in the existing old children for an id set match + const idSetMatch = findIdSetMatch( + oldParent, + newParent, newChild, insertionPoint, ctx, ); - continue; - } + if (idSetMatch) { + insertionPoint = morphChild( + idSetMatch, + newChild, + insertionPoint, + ctx, + ); + continue; + } - // if the current point is already a soft match morph - if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( - insertionPoint, + // if the current point is already a soft match morph + if (isSoftMatch(insertionPoint, newChild)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // search forward in the existing old children for a soft match for the current node + const softMatch = findSoftMatch( + oldParent, + newParent, newChild, insertionPoint, ctx, ); - continue; - } - - // search forward in the existing old children for a soft match for the current node - const softMatch = findSoftMatch( - oldParent, - newParent, - newChild, - insertionPoint, - ctx, - ); - if (softMatch) { - insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); - continue; + if (softMatch) { + insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); + continue; + } } } // last resort, insert a new node from scratch or reuse and morph a remote node with matching id @@ -786,7 +789,6 @@ var Idiomorph = (function () { ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, idMap: idMap, - deadIds: new Set(), persistentIds: persistentIds, pantry: createPantry(), callbacks: mergedConfig.callbacks, @@ -803,21 +805,21 @@ var Idiomorph = (function () { /** * - * @param {Node} node1 - * @param {Node} node2 + * @param {Node} oldNode + * @param {Node} newNode * @param {MorphContext} ctx * @returns {boolean} */ - function isIdSetMatch(node1, node2, ctx) { + function isIdSetMatch(oldNode, newNode, ctx) { if ( - node1 instanceof Element && - node2 instanceof Element && - node1.tagName === node2.tagName + oldNode instanceof Element && + newNode instanceof Element && + oldNode.tagName === newNode.tagName ) { - if (node1.id !== "" && node1.id === node2.id) { + if (oldNode.id !== "" && oldNode.id === newNode.id) { return true; } else { - return getIdIntersectionCount(ctx, node1, node2) > 0; + return getIdIntersectionCount(oldNode, newNode, ctx) > 0; } } return false; @@ -887,7 +889,7 @@ var Idiomorph = (function () { let otherMatchCount = 0; while (potentialMatch != null) { // If we have an id match, return the current potential match - if (isIdSetMatch(newChild, potentialMatch, ctx)) { + if (isIdSetMatch(potentialMatch, newChild, ctx)) { return potentialMatch; } @@ -1049,13 +1051,54 @@ var Idiomorph = (function () { } } + /** + * + * @param {Node} oldNode + * @param {Node} newChild + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function findBestNodeMatch(oldNode, newChild, ctx) { + /** + * @type {Node | null} + */ + let currentNode = newChild; + /** + * @type {Node} + */ + let bestNode = currentNode; + let score = 0; + while (currentNode) { + let newScore = scoreElement(oldNode, currentNode, ctx); + if (newScore > score) { + bestNode = currentNode; + score = newScore; + } + currentNode = currentNode.nextSibling; + } + return bestNode; + } + + /** + * + * @param {Node} oldNode + * @param {Node} newNode + * @param {MorphContext} ctx + * @returns {number} + */ + function scoreElement(oldNode, newNode, ctx) { + if (isSoftMatch(oldNode, newNode)) { + return 0.5 + getPersistentIdNodeCount(ctx, newNode); + } + return 0; + } + /** * * @param {Node} node * @param {MorphContext} ctx */ function removeNode(node, ctx) { - removeIdsFromConsideration(ctx, node); // skip remove callbacks when we're going to be restoring this from the pantry later if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { moveBefore(ctx.pantry, node, null); @@ -1115,41 +1158,6 @@ var Idiomorph = (function () { // ID Set Functions //============================================================================= - /** - * - * @param {MorphContext} ctx - * @param {string} id - * @returns {boolean} - */ - function isIdInConsideration(ctx, id) { - return !ctx.deadIds.has(id); - } - - /** - * - * @param {MorphContext} ctx - * @param {string} id - * @param {Node} targetNode - * @returns {boolean} - */ - function idIsWithinNode(ctx, id, targetNode) { - let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; - return idSet.has(id); - } - - /** - * - * @param {MorphContext} ctx - * @param {Node} node - * @returns {void} - */ - function removeIdsFromConsideration(ctx, node) { - let idSet = ctx.idMap.get(node) || EMPTY_SET; - for (const id of idSet) { - ctx.deadIds.add(id); - } - } - /** * * @param {MorphContext} ctx @@ -1173,18 +1181,22 @@ var Idiomorph = (function () { /** * + * @param {Node} oldNode + * @param {Node} newNode * @param {MorphContext} ctx - * @param {Node} node1 - * @param {Node} node2 * @returns {number} */ - function getIdIntersectionCount(ctx, node1, node2) { - let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; + function getIdIntersectionCount(oldNode, newNode, ctx) { + let oldSet = ctx.idMap.get(oldNode) || EMPTY_SET; + let newSet = ctx.idMap.get(newNode) || EMPTY_SET; + let matchCount = 0; - for (const id of sourceSet) { - // a potential match is an id in the source and potentialIdsSet, but - // that has not already been merged into the DOM - if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { + for (const id of oldSet) { + // a potential match is an id in the new and old nodes that + // has not already been merged into the DOM + // But the newNode content we call this on has not been + // merged yet and we don't allow duplicate IDs so it is simple + if (newSet.has(id)) { ++matchCount; } } From 2580bd0f49bd26f5069901e8f44ca16b2b3303ec Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sun, 12 Jan 2025 13:26:38 +1300 Subject: [PATCH 08/50] handle single node better --- src/idiomorph.js | 62 ++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 17d6f51..fcb9e91 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -204,17 +204,26 @@ var Idiomorph = (function () { return; } } - let normalizedOldNode; if (ctx.morphStyle === "innerHTML") { - normalizedOldNode = oldNode; + morphChildren(oldNode, normalizedNewContent, ctx); + ctx.pantry.remove(); + return Array.from(oldNode.children); } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { - normalizedOldNode = normalizeContent(oldNode); + const normalizedOldNode = normalizeContent(oldNode); + const startPoint = oldNode.previousSibling; + const endPoint = oldNode.nextSibling; + morphChildren(normalizedOldNode, normalizedNewContent, ctx, oldNode); + ctx.pantry.remove(); + let added = []; + let nextSibling = startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; + while (nextSibling != null && nextSibling != endPoint) { + added.push(nextSibling); + nextSibling = nextSibling.nextSibling; + } + return added; } else { throw "Do not understand how to morph style " + ctx.morphStyle; } - morphChildren(normalizedOldNode, normalizedNewContent, ctx); - ctx.pantry.remove(); - return Array.from(normalizedOldNode.children); } /** @@ -342,9 +351,10 @@ var Idiomorph = (function () { * @param {Element} newParent the parent element of the new content * @param {Element} oldParent the old content that we are merging the new content into * @param {MorphContext} ctx the merge context + * @param {Element} [onlyNode] * @returns {void} */ - function morphChildren(oldParent, newParent, ctx) { + function morphChildren(oldParent, newParent, ctx, onlyNode) { if ( oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement @@ -354,16 +364,16 @@ var Idiomorph = (function () { // @ts-ignore ditto newParent = newParent.content; } - - let insertionPoint = /** @type {Node | null} */ (oldParent.firstChild); + let insertionPoint = /** @type {Node | null} */ (onlyNode || oldParent.firstChild); + let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); let bestMatch = /** @type {Node | null} */ (null); // run through all the new content for (const newChild of newParent.childNodes) { - // if we have reached the end of the old parent insertionPoint will be null so skip to end and insert - if (insertionPoint != null) { + // once we reach the end of the old parent content skip to the end and insert + if (insertionPoint != null && insertionPoint != endPoint) { // if last remaining child node then make sure we morph with the best remaining node if there are multiple - if (!insertionPoint.nextSibling && newChild.nextSibling) { + if (onlyNode || (!insertionPoint.nextSibling && newChild.nextSibling)) { bestMatch = findBestNodeMatch(insertionPoint, newChild, ctx); } @@ -371,7 +381,7 @@ var Idiomorph = (function () { if (!bestMatch || bestMatch === newChild) { // if the current node has an id set match then morph if (isIdSetMatch(insertionPoint, newChild, ctx)) { - insertionPoint = morphChild( + insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, @@ -382,14 +392,13 @@ var Idiomorph = (function () { // otherwise search forward in the existing old children for an id set match const idSetMatch = findIdSetMatch( - oldParent, - newParent, newChild, insertionPoint, + endPoint, ctx, ); if (idSetMatch) { - insertionPoint = morphChild( + insertionPoint = morphChild( idSetMatch, newChild, insertionPoint, @@ -400,7 +409,7 @@ var Idiomorph = (function () { // if the current point is already a soft match morph if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( + insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, @@ -411,10 +420,9 @@ var Idiomorph = (function () { // search forward in the existing old children for a soft match for the current node const softMatch = findSoftMatch( - oldParent, - newParent, newChild, insertionPoint, + endPoint, ctx, ); if (softMatch) { @@ -858,18 +866,16 @@ var Idiomorph = (function () { * if the number of potential id matches we are discarding is greater than the * potential id matches for the new child * ============================================================================= - * @param {Node} oldParent - * @param {Node} newContent * @param {Node} newChild * @param {Node | null} insertionPoint + * @param {Node | null} endPoint * @param {MorphContext} ctx * @returns {Node | null} */ function findIdSetMatch( - oldParent, - newContent, newChild, insertionPoint, + endPoint, ctx, ) { // max id matches we are willing to discard in our search @@ -887,7 +893,7 @@ var Idiomorph = (function () { // newChildPotentialIdCount must be greater than this to make it likely // worth it) let otherMatchCount = 0; - while (potentialMatch != null) { + while (potentialMatch != null && potentialMatch != endPoint) { // If we have an id match, return the current potential match if (isIdSetMatch(potentialMatch, newChild, ctx)) { return potentialMatch; @@ -916,14 +922,13 @@ var Idiomorph = (function () { * if we find a potential id match in the old parents children OR if we find two * potential soft matches for the next two pieces of new content * ============================================================================= - * @param {Node} oldParent - * @param {Node} newContent * @param {Node} newChild * @param {Node | null} insertionPoint + * @param {Node | null} endPoint * @param {MorphContext} ctx * @returns {null | Node} */ - function findSoftMatch(oldParent, newContent, newChild, insertionPoint, ctx) { + function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { /** * @type {Node | null} */ @@ -934,7 +939,7 @@ var Idiomorph = (function () { let nextSibling = newChild.nextSibling; let siblingSoftMatchCount = 0; - while (potentialSoftMatch != null) { + while (potentialSoftMatch != null && potentialSoftMatch != endPoint) { if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { // the current potential soft match has a potential id set match with the remaining new // content so bail out of looking @@ -959,7 +964,6 @@ var Idiomorph = (function () { return null; } } - // advanced to the next old content child potentialSoftMatch = potentialSoftMatch.nextSibling; } From 230b8de0ef8ae432d1e2482dc7c92cd62b054564 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Mon, 13 Jan 2025 02:16:44 +1300 Subject: [PATCH 09/50] Improve BestMatch --- src/idiomorph.js | 58 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index fcb9e91..3a1a2d3 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -348,8 +348,8 @@ var Idiomorph = (function () { * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch() and findSoftMatch() for details. * - * @param {Element} newParent the parent element of the new content * @param {Element} oldParent the old content that we are merging the new content into + * @param {Element} newParent the parent element of the new content * @param {MorphContext} ctx the merge context * @param {Element} [onlyNode] * @returns {void} @@ -366,22 +366,40 @@ var Idiomorph = (function () { } let insertionPoint = /** @type {Node | null} */ (onlyNode || oldParent.firstChild); let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); + + // Find the last Node with Ids to be used to find final best match let bestMatch = /** @type {Node | null} */ (null); + let lastNodeWithIds = /** @type {Node | null} */ (null); + // @ts-ignore check for moveBefore existance + if (!oldParent.moveBefore) { + if (onlyNode) { + if (hasPersistentIdNodes(ctx, onlyNode)) { + lastNodeWithIds = onlyNode; + } + } else { + lastNodeWithIds = oldParent.lastChild; + while (lastNodeWithIds && !hasPersistentIdNodes(ctx, lastNodeWithIds)) { + lastNodeWithIds = lastNodeWithIds.previousSibling + } + } + } // run through all the new content for (const newChild of newParent.childNodes) { // once we reach the end of the old parent content skip to the end and insert if (insertionPoint != null && insertionPoint != endPoint) { - // if last remaining child node then make sure we morph with the best remaining node if there are multiple - if (onlyNode || (!insertionPoint.nextSibling && newChild.nextSibling)) { + // if last remaining child node with Ids then make sure we morph with the best remaining node if there are multiple + if (!bestMatch && insertionPoint == lastNodeWithIds) { bestMatch = findBestNodeMatch(insertionPoint, newChild, ctx); } - + // if(bestMatch) console.log(bestMatch.outerHTML) // if there is no bestMatch or we have found the bestMatch then morph, else skip to end and insert if (!bestMatch || bestMatch === newChild) { + // clear bestMatch if set + bestMatch = null; // if the current node has an id set match then morph if (isIdSetMatch(insertionPoint, newChild, ctx)) { - insertionPoint = morphChild( + insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, @@ -398,7 +416,8 @@ var Idiomorph = (function () { ctx, ); if (idSetMatch) { - insertionPoint = morphChild( + //insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); + insertionPoint = morphChild( idSetMatch, newChild, insertionPoint, @@ -409,7 +428,7 @@ var Idiomorph = (function () { // if the current point is already a soft match morph if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( + insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, @@ -426,6 +445,7 @@ var Idiomorph = (function () { ctx, ); if (softMatch) { + //insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); continue; } @@ -436,7 +456,7 @@ var Idiomorph = (function () { } // remove any remaining old nodes that didn't match up with new content - while (insertionPoint) { + while (insertionPoint && insertionPoint != endPoint) { const tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); @@ -968,7 +988,7 @@ var Idiomorph = (function () { potentialSoftMatch = potentialSoftMatch.nextSibling; } - return potentialSoftMatch; + return null; } /** @type {WeakSet} */ @@ -1097,6 +1117,26 @@ var Idiomorph = (function () { return 0; } + /** + * + * @param {Node} startInclusive + * @param {Node} endExclusive + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function removeNodesBetween(startInclusive, endExclusive, ctx) { + /** @type {Node | null} */ let cursor = startInclusive; + while (cursor && cursor !== endExclusive) { // } && !hasPersistentIdNodes(ctx, cursor)) { + let tempNode = /** @type {Node} */ (cursor); + // TODO: Prefer assigning to a new variable here or expand the type of startInclusive + // to be Node | null + cursor = tempNode.nextSibling; + removeNode(tempNode, ctx); + } + //removeIdsFromConsideration(ctx, endExclusive); + return cursor; + } + /** * * @param {Node} node From 7f9e4e58aec3f79b0d2620f674e9e5941160a646 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 18:15:29 -0600 Subject: [PATCH 10/50] cleanup. --- src/idiomorph.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 3a1a2d3..512be3b 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -346,7 +346,7 @@ var Idiomorph = (function () { * - otherwise, prepend the new node before the current insertion point * * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved - * with the current node. See findIdSetMatch() and findSoftMatch() for details. + * with the current node. See findIdSetMatch and findSoftMatch for details. * * @param {Element} oldParent the old content that we are merging the new content into * @param {Element} newParent the parent element of the new content @@ -713,17 +713,14 @@ var Idiomorph = (function () { // Push the remaining new head elements in the Map into the // nodes to append to the head tag nodesToAppend.push(...srcToNewHeadNodes.values()); - log("to append: ", nodesToAppend); let promises = []; for (const newNode of nodesToAppend) { - log("adding: ", newNode); // TODO: This could theoretically be null, based on type let newElt = /** @type {ChildNode} */ ( document.createRange().createContextualFragment(newNode.outerHTML) .firstChild ); - log(newElt); if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { if ( ("href" in newElt && newElt.href) || @@ -762,13 +759,6 @@ var Idiomorph = (function () { // Misc //============================================================================= - /** - * @param {any[]} _args - */ - function log(..._args) { - //console.log(args); - } - function noOp() {} /** From abc3abd714528de17a9d5c512f9d71c6714c891e Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 11:39:32 -0600 Subject: [PATCH 11/50] remove bespoke outerHTML morphing functions. --- src/idiomorph.js | 169 +++++++++++------------------------------------ 1 file changed, 40 insertions(+), 129 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 512be3b..72a7a28 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -367,88 +367,61 @@ var Idiomorph = (function () { let insertionPoint = /** @type {Node | null} */ (onlyNode || oldParent.firstChild); let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); - // Find the last Node with Ids to be used to find final best match - let bestMatch = /** @type {Node | null} */ (null); - let lastNodeWithIds = /** @type {Node | null} */ (null); - // @ts-ignore check for moveBefore existance - if (!oldParent.moveBefore) { - if (onlyNode) { - if (hasPersistentIdNodes(ctx, onlyNode)) { - lastNodeWithIds = onlyNode; - } - } else { - lastNodeWithIds = oldParent.lastChild; - while (lastNodeWithIds && !hasPersistentIdNodes(ctx, lastNodeWithIds)) { - lastNodeWithIds = lastNodeWithIds.previousSibling - } - } - } - // run through all the new content for (const newChild of newParent.childNodes) { // once we reach the end of the old parent content skip to the end and insert if (insertionPoint != null && insertionPoint != endPoint) { - // if last remaining child node with Ids then make sure we morph with the best remaining node if there are multiple - if (!bestMatch && insertionPoint == lastNodeWithIds) { - bestMatch = findBestNodeMatch(insertionPoint, newChild, ctx); + // if the current node has an id set match then morph + if (isIdSetMatch(insertionPoint, newChild, ctx)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; } - // if(bestMatch) console.log(bestMatch.outerHTML) - // if there is no bestMatch or we have found the bestMatch then morph, else skip to end and insert - if (!bestMatch || bestMatch === newChild) { - // clear bestMatch if set - bestMatch = null; - // if the current node has an id set match then morph - if (isIdSetMatch(insertionPoint, newChild, ctx)) { - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - continue; - } - // otherwise search forward in the existing old children for an id set match - const idSetMatch = findIdSetMatch( + // otherwise search forward in the existing old children for an id set match + const idSetMatch = findIdSetMatch( + newChild, + insertionPoint, + endPoint, + ctx, + ); + if (idSetMatch) { + //insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); + insertionPoint = morphChild( + idSetMatch, newChild, insertionPoint, - endPoint, ctx, ); - if (idSetMatch) { - //insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); - insertionPoint = morphChild( - idSetMatch, - newChild, - insertionPoint, - ctx, - ); - continue; - } - - // if the current point is already a soft match morph - if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - continue; - } + continue; + } - // search forward in the existing old children for a soft match for the current node - const softMatch = findSoftMatch( + // if the current point is already a soft match morph + if (isSoftMatch(insertionPoint, newChild)) { + insertionPoint = morphChild( + insertionPoint, newChild, insertionPoint, - endPoint, ctx, ); - if (softMatch) { - //insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); - insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); - continue; - } + continue; + } + + // search forward in the existing old children for a soft match for the current node + const softMatch = findSoftMatch( + newChild, + insertionPoint, + endPoint, + ctx, + ); + if (softMatch) { + //insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); + insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); + continue; } } // last resort, insert a new node from scratch or reuse and morph a remote node with matching id @@ -1065,68 +1038,6 @@ var Idiomorph = (function () { } } - /** - * - * @param {Node} oldNode - * @param {Node} newChild - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function findBestNodeMatch(oldNode, newChild, ctx) { - /** - * @type {Node | null} - */ - let currentNode = newChild; - /** - * @type {Node} - */ - let bestNode = currentNode; - let score = 0; - while (currentNode) { - let newScore = scoreElement(oldNode, currentNode, ctx); - if (newScore > score) { - bestNode = currentNode; - score = newScore; - } - currentNode = currentNode.nextSibling; - } - return bestNode; - } - - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {number} - */ - function scoreElement(oldNode, newNode, ctx) { - if (isSoftMatch(oldNode, newNode)) { - return 0.5 + getPersistentIdNodeCount(ctx, newNode); - } - return 0; - } - - /** - * - * @param {Node} startInclusive - * @param {Node} endExclusive - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function removeNodesBetween(startInclusive, endExclusive, ctx) { - /** @type {Node | null} */ let cursor = startInclusive; - while (cursor && cursor !== endExclusive) { // } && !hasPersistentIdNodes(ctx, cursor)) { - let tempNode = /** @type {Node} */ (cursor); - // TODO: Prefer assigning to a new variable here or expand the type of startInclusive - // to be Node | null - cursor = tempNode.nextSibling; - removeNode(tempNode, ctx); - } - //removeIdsFromConsideration(ctx, endExclusive); - return cursor; - } - /** * * @param {Node} node From e4e0aa2150974be5bb5cf01fef9caf589a618586 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 23:25:26 -0600 Subject: [PATCH 12/50] rename fns. --- src/idiomorph.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 72a7a28..fd515aa 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -151,7 +151,7 @@ var Idiomorph = (function () { /** * ============================================================================= - * Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren + * Core Morphing Algorithm - morph, morphNormalizedContent, morphNode, morphChildren * ============================================================================= * * @param {Element | Document} oldNode @@ -255,14 +255,14 @@ var Idiomorph = (function () { insertionPoint, ctx, ); - morphOldNodeTo(movedChild, newChild, ctx); + morphNode(movedChild, newChild, ctx); } else { if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm const newEmptyChild = document.createElement(newChild.tagName); oldParent.insertBefore(newEmptyChild, insertionPoint); - morphOldNodeTo(newEmptyChild, newChild, ctx); + morphNode(newEmptyChild, newChild, ctx); ctx.callbacks.afterNodeAdded(newEmptyChild); } else { // no id state to preserve so just insert a clone of the new data to avoid mutating newParent @@ -279,7 +279,7 @@ var Idiomorph = (function () { * @param {MorphContext} ctx the merge context * @returns {Node | null} the element that ended up in the DOM */ - function morphOldNodeTo(oldNode, newContent, ctx) { + function morphNode(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) { // don't morph focused element return null; @@ -302,7 +302,7 @@ var Idiomorph = (function () { ctx, ); } else { - syncNode(oldNode, newContent, ctx); + morphAttributes(oldNode, newContent, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { // @ts-ignore newContent can be a node here because .firstChild will be null morphChildren(oldNode, newContent, ctx); @@ -325,7 +325,7 @@ var Idiomorph = (function () { // @ts-ignore we know the Node has a valid parent moveBefore(oldNode.parentElement, oldNode, insertionPoint); } - morphOldNodeTo(oldNode, newChild, ctx); + morphNode(oldNode, newChild, ctx); return oldNode.nextSibling; } @@ -469,7 +469,7 @@ var Idiomorph = (function () { * @param {MorphContext} ctx the merge context */ - function syncNode(oldNode, newNode, ctx) { + function morphAttributes(oldNode, newNode, ctx) { let type = newNode.nodeType; // if is an element type, sync the attributes from the From 31456d80eb1d3c0bdc5a62b900d3a58a4071f9ae Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 23:37:44 -0600 Subject: [PATCH 13/50] create submodule for createMorphContext. --- src/idiomorph.js | 334 ++++++++++++++++++++++++----------------------- 1 file changed, 168 insertions(+), 166 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index fd515aa..e054d70 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -215,7 +215,8 @@ var Idiomorph = (function () { morphChildren(normalizedOldNode, normalizedNewContent, ctx, oldNode); ctx.pantry.remove(); let added = []; - let nextSibling = startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; + let nextSibling = + startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; while (nextSibling != null && nextSibling != endPoint) { added.push(nextSibling); nextSibling = nextSibling.nextSibling; @@ -364,7 +365,9 @@ var Idiomorph = (function () { // @ts-ignore ditto newParent = newParent.content; } - let insertionPoint = /** @type {Node | null} */ (onlyNode || oldParent.firstChild); + let insertionPoint = /** @type {Node | null} */ ( + onlyNode || oldParent.firstChild + ); let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); // run through all the new content @@ -734,65 +737,175 @@ var Idiomorph = (function () { function noOp() {} - /** - * Deep merges the config object and the Idiomoroph.defaults object to - * produce a final configuration object - * @param {Config} config - * @returns {ConfigInternal} - */ - function mergeDefaults(config) { + const createMorphContext = (function () { /** - * @type {ConfigInternal} + * + * @param {Element} oldNode + * @param {Element} newContent + * @param {Config} config + * @returns {MorphContext} */ - let finalConfig = Object.assign({}, defaults); + function createMorphContext(oldNode, newContent, config) { + const mergedConfig = mergeDefaults(config); + const { persistentIds, idMap } = createIdMaps(oldNode, newContent); + return { + target: oldNode, + newContent: newContent, + config: mergedConfig, + morphStyle: mergedConfig.morphStyle, + ignoreActive: mergedConfig.ignoreActive, + ignoreActiveValue: mergedConfig.ignoreActiveValue, + idMap: idMap, + persistentIds: persistentIds, + pantry: createPantry(), + callbacks: mergedConfig.callbacks, + head: mergedConfig.head, + }; + } - // copy top level stuff into final config - Object.assign(finalConfig, config); + /** + * Deep merges the config object and the Idiomorph.defaults object to + * produce a final configuration object + * @param {Config} config + * @returns {ConfigInternal} + */ + function mergeDefaults(config) { + /** + * @type {ConfigInternal} + */ + let finalConfig = Object.assign({}, defaults); + + // copy top level stuff into final config + Object.assign(finalConfig, config); + + // copy callbacks into final config (do this to deep merge the callbacks) + finalConfig.callbacks = Object.assign( + {}, + defaults.callbacks, + config.callbacks, + ); - // copy callbacks into final config (do this to deep merge the callbacks) - finalConfig.callbacks = Object.assign( - {}, - defaults.callbacks, - config.callbacks, - ); + // copy head config into final config (do this to deep merge the head) + finalConfig.head = Object.assign({}, defaults.head, config.head); - // copy head config into final config (do this to deep merge the head) - finalConfig.head = Object.assign({}, defaults.head, config.head); + return finalConfig; + } - return finalConfig; - } + function createPantry() { + const pantry = document.createElement("div"); + pantry.hidden = true; + document.body.insertAdjacentElement("afterend", pantry); + return pantry; + } - /** - * - * @param {Element} oldNode - * @param {Element} newContent - * @param {Config} config - * @returns {MorphContext} - */ - function createMorphContext(oldNode, newContent, config) { - const mergedConfig = mergeDefaults(config); - const { persistentIds, idMap } = createIdMaps(oldNode, newContent); - return { - target: oldNode, - newContent: newContent, - config: mergedConfig, - morphStyle: mergedConfig.morphStyle, - ignoreActive: mergedConfig.ignoreActive, - ignoreActiveValue: mergedConfig.ignoreActiveValue, - idMap: idMap, - persistentIds: persistentIds, - pantry: createPantry(), - callbacks: mergedConfig.callbacks, - head: mergedConfig.head, - }; - } + /** + * @param {Element} content + * @returns {Element[]} + */ + function elementsWithIds(content) { + let elements = Array.from(content.querySelectorAll("[id]")); + if (content.id) { + elements.push(content); + } + return elements; + } - function createPantry() { - const pantry = document.createElement("div"); - pantry.hidden = true; - document.body.insertAdjacentElement("afterend", pantry); - return pantry; - } + /** + * A bottom up algorithm that finds all elements with ids in the node + * argument and populates id sets for those nodes and all their parents, generating + * a set of ids contained within all nodes for the entire hierarchy in the DOM + * + * @param {Element|null} nodeParent + * @param {Element[]} nodes + * @param {Set} persistentIds + * @param {Map>} idMap + */ + function populateIdMapForNode(nodeParent, nodes, persistentIds, idMap) { + for (const elt of nodes) { + if (persistentIds.has(elt.id)) { + /** + * @type {Element|null} + */ + let current = elt; + // walk up the parent hierarchy of that element, adding the id + // of element to the parent's id set + while (current !== nodeParent && current != null) { + let idSet = idMap.get(current); + // if the id set doesn't exist, create it and insert it in the map + if (idSet == null) { + idSet = new Set(); + idMap.set(current, idSet); + } + idSet.add(elt.id); + current = current.parentElement; + } + } + } + } + + /** + * This function computes a map of nodes to all ids contained within that node (inclusive of the + * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows + * for a looser definition of "matching" than tradition id matching, and allows child nodes + * to contribute to a parent nodes matching. + * + * @param {Element} oldContent the old content that will be morphed + * @param {Element} newContent the new content to morph to + * @returns {IdSets} a map of nodes to id sets for the + */ + function createIdMaps(oldContent, newContent) { + // Calculate ids that persist between the two contents exculuding duplicates first + let oldIdMap = new Map(); + let dupSet = new Set(); + const oldElts = elementsWithIds(oldContent); + for (const oldElt of oldElts) { + const id = oldElt.id; + // if already in map then log duplicates to be skipped + if (oldIdMap.get(id)) { + dupSet.add(id); + } else { + oldIdMap.set(id, oldElt.tagName); + } + } + let persistentIds = new Set(); + const newElts = elementsWithIds(newContent); + for (const newElt of newElts) { + const id = newElt.id; + const oldTagName = oldIdMap.get(id); + // if already matched skip id as duplicate but also skip if tag types mismatch because it could match later + if ( + persistentIds.has(id) || + (oldTagName && oldTagName !== newElt.tagName) + ) { + dupSet.add(id); + persistentIds.delete(id); + } + if (oldTagName === newElt.tagName && !dupSet.has(id)) { + persistentIds.add(id); + } + } + /** + * + * @type {Map>} + */ + let idMap = new Map(); + populateIdMapForNode( + oldContent.parentElement, + newElts, + persistentIds, + idMap, + ); + populateIdMapForNode( + newContent.parentElement, + oldElts, + persistentIds, + idMap, + ); + return { persistentIds, idMap }; + } + + return createMorphContext; + })(); /** * @@ -855,12 +968,7 @@ var Idiomorph = (function () { * @param {MorphContext} ctx * @returns {Node | null} */ - function findIdSetMatch( - newChild, - insertionPoint, - endPoint, - ctx, - ) { + function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { // max id matches we are willing to discard in our search let newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); @@ -1134,7 +1242,7 @@ var Idiomorph = (function () { function getIdIntersectionCount(oldNode, newNode, ctx) { let oldSet = ctx.idMap.get(oldNode) || EMPTY_SET; let newSet = ctx.idMap.get(newNode) || EMPTY_SET; - + let matchCount = 0; for (const id of oldSet) { // a potential match is an id in the new and old nodes that @@ -1148,112 +1256,6 @@ var Idiomorph = (function () { return matchCount; } - /** - * @param {Element} content - * @returns {Element[]} - */ - function elementsWithIds(content) { - let elements = Array.from(content.querySelectorAll("[id]")); - if (content.id) { - elements.push(content); - } - return elements; - } - - /** - * A bottom up algorithm that finds all elements with ids in the node - * argument and populates id sets for those nodes and all their parents, generating - * a set of ids contained within all nodes for the entire hierarchy in the DOM - * - * @param {Element|null} nodeParent - * @param {Element[]} nodes - * @param {Set} persistentIds - * @param {Map>} idMap - */ - function populateIdMapForNode(nodeParent, nodes, persistentIds, idMap) { - for (const elt of nodes) { - if (persistentIds.has(elt.id)) { - /** - * @type {Element|null} - */ - let current = elt; - // walk up the parent hierarchy of that element, adding the id - // of element to the parent's id set - while (current !== nodeParent && current != null) { - let idSet = idMap.get(current); - // if the id set doesn't exist, create it and insert it in the map - if (idSet == null) { - idSet = new Set(); - idMap.set(current, idSet); - } - idSet.add(elt.id); - current = current.parentElement; - } - } - } - } - - /** - * This function computes a map of nodes to all ids contained within that node (inclusive of the - * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows - * for a looser definition of "matching" than tradition id matching, and allows child nodes - * to contribute to a parent nodes matching. - * - * @param {Element} oldContent the old content that will be morphed - * @param {Element} newContent the new content to morph to - * @returns {IdSets} a map of nodes to id sets for the - */ - function createIdMaps(oldContent, newContent) { - // Calculate ids that persist between the two contents exculuding duplicates first - let oldIdMap = new Map(); - let dupSet = new Set(); - const oldElts = elementsWithIds(oldContent); - for (const oldElt of oldElts) { - const id = oldElt.id; - // if already in map then log duplicates to be skipped - if (oldIdMap.get(id)) { - dupSet.add(id); - } else { - oldIdMap.set(id, oldElt.tagName); - } - } - let persistentIds = new Set(); - const newElts = elementsWithIds(newContent); - for (const newElt of newElts) { - const id = newElt.id; - const oldTagName = oldIdMap.get(id); - // if already matched skip id as duplicate but also skip if tag types mismatch because it could match later - if ( - persistentIds.has(id) || - (oldTagName && oldTagName !== newElt.tagName) - ) { - dupSet.add(id); - persistentIds.delete(id); - } - if (oldTagName === newElt.tagName && !dupSet.has(id)) { - persistentIds.add(id); - } - } - /** - * - * @type {Map>} - */ - let idMap = new Map(); - populateIdMapForNode( - oldContent.parentElement, - newElts, - persistentIds, - idMap, - ); - populateIdMapForNode( - newContent.parentElement, - oldElts, - persistentIds, - idMap, - ); - return { persistentIds, idMap }; - } - //============================================================================= // This is what ends up becoming the Idiomorph global object //============================================================================= From c0d127dd012a938bd86a276ebba83c38ab2687a8 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 23:44:16 -0600 Subject: [PATCH 14/50] parsing a string is normalizing. --- src/idiomorph.js | 158 ++++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index e054d70..4f3f256 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -164,10 +164,6 @@ var Idiomorph = (function () { oldNode = oldNode.documentElement; } - if (typeof newContent === "string") { - newContent = parseContent(newContent); - } - let normalizedContent = normalizeContent(newContent); let ctx = createMorphContext(oldNode, normalizedContent, config); @@ -1062,89 +1058,95 @@ var Idiomorph = (function () { return null; } - /** @type {WeakSet} */ - const generatedByIdiomorph = new WeakSet(); - - /** - * - * @param {string} newContent - * @returns {Node | null | DocumentFragment} - */ - function parseContent(newContent) { - let parser = new DOMParser(); - - // remove svgs to avoid false-positive matches on head, etc. - let contentWithSvgsRemoved = newContent.replace( - /]*>|>)([\s\S]*?)<\/svg>/gim, - "", - ); + const normalizeContent = (function() { + /** @type {WeakSet} */ + const generatedByIdiomorph = new WeakSet(); - // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping - if ( - contentWithSvgsRemoved.match(/<\/html>/) || - contentWithSvgsRemoved.match(/<\/head>/) || - contentWithSvgsRemoved.match(/<\/body>/) - ) { - let content = parser.parseFromString(newContent, "text/html"); - // if it is a full HTML document, return the document itself as the parent container - if (contentWithSvgsRemoved.match(/<\/html>/)) { - generatedByIdiomorph.add(content); - return content; + /** + * + * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent + * @returns {Element} + */ + function normalizeContent(newContent) { + if (newContent == null) { + // noinspection UnnecessaryLocalVariableJS + const dummyParent = document.createElement("div"); + return dummyParent; + } else if (typeof newContent === "string") { + return normalizeContent(parseContent(newContent)); + } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) { + // the template tag created by idiomorph parsing can serve as a dummy parent + return /** @type {Element} */ (newContent); + } else if (newContent instanceof Node) { + if (newContent.parentNode) { + return /** @type {Element} */ (newContent.parentNode); + } else { + // a single node is added as a child to a dummy parent + const dummyParent = document.createElement("div"); + dummyParent.append(newContent); + return dummyParent; + } } else { - // otherwise return the html element as the parent container - let htmlElement = content.firstChild; - if (htmlElement) { - generatedByIdiomorph.add(htmlElement); + // all nodes in the array or HTMLElement collection are consolidated under + // a single dummy parent element + const dummyParent = document.createElement("div"); + for (const elt of [...newContent]) { + dummyParent.append(elt); } - return htmlElement; + return dummyParent; } - } else { - // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help - // deal with touchy tags like tr, tbody, etc. - let responseDoc = parser.parseFromString( - "", - "text/html", - ); - let content = /** @type {HTMLTemplateElement} */ ( - responseDoc.body.querySelector("template") - ).content; - generatedByIdiomorph.add(content); - return content; } - } - /** - * - * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent - * @returns {Element} - */ - function normalizeContent(newContent) { - if (newContent == null) { - // noinspection UnnecessaryLocalVariableJS - const dummyParent = document.createElement("div"); - return dummyParent; - } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) { - // the template tag created by idiomorph parsing can serve as a dummy parent - return /** @type {Element} */ (newContent); - } else if (newContent instanceof Node) { - if (newContent.parentNode) { - return /** @type {Element} */ (newContent.parentNode); + /** + * + * @param {string} newContent + * @returns {Node | null | DocumentFragment} + */ + function parseContent(newContent) { + let parser = new DOMParser(); + + // remove svgs to avoid false-positive matches on head, etc. + let contentWithSvgsRemoved = newContent.replace( + /]*>|>)([\s\S]*?)<\/svg>/gim, + "", + ); + + // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping + if ( + contentWithSvgsRemoved.match(/<\/html>/) || + contentWithSvgsRemoved.match(/<\/head>/) || + contentWithSvgsRemoved.match(/<\/body>/) + ) { + let content = parser.parseFromString(newContent, "text/html"); + // if it is a full HTML document, return the document itself as the parent container + if (contentWithSvgsRemoved.match(/<\/html>/)) { + generatedByIdiomorph.add(content); + return content; + } else { + // otherwise return the html element as the parent container + let htmlElement = content.firstChild; + if (htmlElement) { + generatedByIdiomorph.add(htmlElement); + } + return htmlElement; + } } else { - // a single node is added as a child to a dummy parent - const dummyParent = document.createElement("div"); - dummyParent.append(newContent); - return dummyParent; - } - } else { - // all nodes in the array or HTMLElement collection are consolidated under - // a single dummy parent element - const dummyParent = document.createElement("div"); - for (const elt of [...newContent]) { - dummyParent.append(elt); + // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help + // deal with touchy tags like tr, tbody, etc. + let responseDoc = parser.parseFromString( + "", + "text/html", + ); + let content = /** @type {HTMLTemplateElement} */ ( + responseDoc.body.querySelector("template") + ).content; + generatedByIdiomorph.add(content); + return content; } - return dummyParent; } - } + + return normalizeContent; + })(); /** * From d414cc218a1716a5e6c51909b99a4c589598b3ae Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 23:51:08 -0600 Subject: [PATCH 15/50] extract morphNode module. --- src/idiomorph.js | 450 ++++++++++++++++++++++++----------------------- 1 file changed, 226 insertions(+), 224 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 4f3f256..fd899e6 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -223,19 +223,6 @@ var Idiomorph = (function () { } } - /** - * @param {Node} possibleActiveElement - * @param {MorphContext} ctx - * @returns {boolean} - */ - function ignoreValueOfActiveElement(possibleActiveElement, ctx) { - return ( - !!ctx.ignoreActiveValue && - possibleActiveElement === document.activeElement && - possibleActiveElement !== document.body - ); - } - /** * @param {Element | null} oldParent * @param {Node} newChild new content to merge @@ -270,44 +257,238 @@ var Idiomorph = (function () { } } - /** - * @param {Node} oldNode root node to merge content into - * @param {Node} newContent new content to merge - * @param {MorphContext} ctx the merge context - * @returns {Node | null} the element that ended up in the DOM - */ - function morphNode(oldNode, newContent, ctx) { - if (ctx.ignoreActive && oldNode === document.activeElement) { - // don't morph focused element - return null; - } + //============================================================================= + // Single Node Morphing Code + //============================================================================= + const morphNode = (function() { + /** + * @param {Node} oldNode root node to merge content into + * @param {Node} newContent new content to merge + * @param {MorphContext} ctx the merge context + * @returns {Node | null} the element that ended up in the DOM + */ + function morphNode(oldNode, newContent, ctx) { + if (ctx.ignoreActive && oldNode === document.activeElement) { + // don't morph focused element + return null; + } + + if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) { + return oldNode; + } - if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) { + if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { + // ignore the head element + } else if ( + oldNode instanceof HTMLHeadElement && + ctx.head.style !== "morph" + ) { + // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above + handleHeadElement( + oldNode, + /** @type {HTMLHeadElement} */ (newContent), + ctx, + ); + } else { + morphAttributes(oldNode, newContent, ctx); + if (!ignoreValueOfActiveElement(oldNode, ctx)) { + // @ts-ignore newContent can be a node here because .firstChild will be null + morphChildren(oldNode, newContent, ctx); + } + } + ctx.callbacks.afterNodeMorphed(oldNode, newContent); return oldNode; } - if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { - // ignore the head element - } else if ( - oldNode instanceof HTMLHeadElement && - ctx.head.style !== "morph" - ) { - // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above - handleHeadElement( - oldNode, - /** @type {HTMLHeadElement} */ (newContent), - ctx, - ); - } else { - morphAttributes(oldNode, newContent, ctx); - if (!ignoreValueOfActiveElement(oldNode, ctx)) { - // @ts-ignore newContent can be a node here because .firstChild will be null - morphChildren(oldNode, newContent, ctx); + /** + * syncs a given node with another node, copying over all attributes and + * inner element state from the newNode to the oldNode + * + * @param {Node} oldNode the node to copy attributes & state to + * @param {Node} newNode the node to copy attributes & state from + * @param {MorphContext} ctx the merge context + */ + function morphAttributes(oldNode, newNode, ctx) { + let type = newNode.nodeType; + + // if is an element type, sync the attributes from the + // new node into the new node + if (type === 1 /* element type */) { + const oldElt = /** @type {Element} */ (oldNode); + const newElt = /** @type {Element} */ (newNode); + + const oldAttributes = oldElt.attributes; + const newAttributes = newElt.attributes; + for (const newAttribute of newAttributes) { + if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { + continue; + } + if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) { + oldElt.setAttribute(newAttribute.name, newAttribute.value); + } + } + // iterate backwards to avoid skipping over items when a delete occurs + for (let i = oldAttributes.length - 1; 0 <= i; i--) { + const oldAttribute = oldAttributes[i]; + + // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe + // e.g. custom element attribute callbacks can remove other attributes + if (!oldAttribute) continue; + + if (!newElt.hasAttribute(oldAttribute.name)) { + if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) { + continue; + } + oldElt.removeAttribute(oldAttribute.name); + } + } + + if (!ignoreValueOfActiveElement(oldElt, ctx)) { + syncInputValue(oldElt, newElt, ctx); + } + } + + // sync text nodes + if (type === 8 /* comment */ || type === 3 /* text */) { + if (oldNode.nodeValue !== newNode.nodeValue) { + oldNode.nodeValue = newNode.nodeValue; + } } } - ctx.callbacks.afterNodeMorphed(oldNode, newContent); - return oldNode; - } + + /** + * NB: many bothans died to bring us information: + * + * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js + * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 + * + * @param {Element} oldElement the element to sync the input value to + * @param {Element} newElement the element to sync the input value from + * @param {MorphContext} ctx the merge context + */ + function syncInputValue(oldElement, newElement, ctx) { + if ( + oldElement instanceof HTMLInputElement && + newElement instanceof HTMLInputElement && + newElement.type !== "file" + ) { + let newValue = newElement.value; + let oldValue = oldElement.value; + + // sync boolean attributes + syncBooleanAttribute(oldElement, newElement, "checked", ctx); + syncBooleanAttribute(oldElement, newElement, "disabled", ctx); + + if (!newElement.hasAttribute("value")) { + if (!ignoreAttribute("value", oldElement, "remove", ctx)) { + oldElement.value = ""; + oldElement.removeAttribute("value"); + } + } else if (oldValue !== newValue) { + if (!ignoreAttribute("value", oldElement, "update", ctx)) { + oldElement.setAttribute("value", newValue); + oldElement.value = newValue; + } + } + // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why? + // did I break something? + } else if ( + oldElement instanceof HTMLOptionElement && + newElement instanceof HTMLOptionElement + ) { + syncBooleanAttribute(oldElement, newElement, "selected", ctx); + } else if ( + oldElement instanceof HTMLTextAreaElement && + newElement instanceof HTMLTextAreaElement + ) { + let newValue = newElement.value; + let oldValue = oldElement.value; + if (ignoreAttribute("value", oldElement, "update", ctx)) { + return; + } + if (newValue !== oldValue) { + oldElement.value = newValue; + } + if ( + oldElement.firstChild && + oldElement.firstChild.nodeValue !== newValue + ) { + oldElement.firstChild.nodeValue = newValue; + } + } + } + + /** + * @param {Element} oldElement element to sync the value to + * @param {Element} newElement element to sync the value from + * @param {string} attributeName the attribute name + * @param {MorphContext} ctx the merge context + */ + function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + const newLiveValue = newElement[attributeName], + // @ts-ignore ditto + oldLiveValue = oldElement[attributeName]; + if (newLiveValue !== oldLiveValue) { + const ignoreUpdate = ignoreAttribute( + attributeName, + oldElement, + "update", + ctx, + ); + if (!ignoreUpdate) { + // update attribute's associated DOM property + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + oldElement[attributeName] = newElement[attributeName]; + } + if (newLiveValue) { + if (!ignoreUpdate) { + // TODO: do we really want this? tests say so but it feels wrong + oldElement.setAttribute(attributeName, newLiveValue); + } + } else { + if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { + oldElement.removeAttribute(attributeName); + } + } + } + } + + /** + * @param {string} attr the attribute to be mutated + * @param {Element} element the element that is going to be updated + * @param {"update" | "remove"} updateType + * @param {MorphContext} ctx the merge context + * @returns {boolean} true if the attribute should be ignored, false otherwise + */ + function ignoreAttribute(attr, element, updateType, ctx) { + if ( + attr === "value" && + ctx.ignoreActiveValue && + element === document.activeElement + ) { + return true; + } + return ( + ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false + ); + } + + /** + * @param {Node} possibleActiveElement + * @param {MorphContext} ctx + * @returns {boolean} + */ + function ignoreValueOfActiveElement(possibleActiveElement, ctx) { + return ( + !!ctx.ignoreActiveValue && + possibleActiveElement === document.activeElement && + possibleActiveElement !== document.body + ); + } + + return morphNode; + })(); /** * @param {Node} oldNode the node to be morphed @@ -435,185 +616,6 @@ var Idiomorph = (function () { } } - //============================================================================= - // Attribute Syncing Code - //============================================================================= - - /** - * @param {string} attr the attribute to be mutated - * @param {Element} element the element that is going to be updated - * @param {"update" | "remove"} updateType - * @param {MorphContext} ctx the merge context - * @returns {boolean} true if the attribute should be ignored, false otherwise - */ - function ignoreAttribute(attr, element, updateType, ctx) { - if ( - attr === "value" && - ctx.ignoreActiveValue && - element === document.activeElement - ) { - return true; - } - return ( - ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false - ); - } - - /** - * syncs a given node with another node, copying over all attributes and - * inner element state from the newNode to the oldNode - * - * @param {Node} oldNode the node to copy attributes & state to - * @param {Node} newNode the node to copy attributes & state from - * @param {MorphContext} ctx the merge context - */ - - function morphAttributes(oldNode, newNode, ctx) { - let type = newNode.nodeType; - - // if is an element type, sync the attributes from the - // new node into the new node - if (type === 1 /* element type */) { - const oldElt = /** @type {Element} */ (oldNode); - const newElt = /** @type {Element} */ (newNode); - - const oldAttributes = oldElt.attributes; - const newAttributes = newElt.attributes; - for (const newAttribute of newAttributes) { - if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { - continue; - } - if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) { - oldElt.setAttribute(newAttribute.name, newAttribute.value); - } - } - // iterate backwards to avoid skipping over items when a delete occurs - for (let i = oldAttributes.length - 1; 0 <= i; i--) { - const oldAttribute = oldAttributes[i]; - - // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe - // e.g. custom element attribute callbacks can remove other attributes - if (!oldAttribute) continue; - - if (!newElt.hasAttribute(oldAttribute.name)) { - if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) { - continue; - } - oldElt.removeAttribute(oldAttribute.name); - } - } - - if (!ignoreValueOfActiveElement(oldElt, ctx)) { - syncInputValue(oldElt, newElt, ctx); - } - } - - // sync text nodes - if (type === 8 /* comment */ || type === 3 /* text */) { - if (oldNode.nodeValue !== newNode.nodeValue) { - oldNode.nodeValue = newNode.nodeValue; - } - } - } - - /** - * NB: many bothans died to bring us information: - * - * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js - * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 - * - * @param {Element} oldElement the element to sync the input value to - * @param {Element} newElement the element to sync the input value from - * @param {MorphContext} ctx the merge context - */ - function syncInputValue(oldElement, newElement, ctx) { - if ( - oldElement instanceof HTMLInputElement && - newElement instanceof HTMLInputElement && - newElement.type !== "file" - ) { - let newValue = newElement.value; - let oldValue = oldElement.value; - - // sync boolean attributes - syncBooleanAttribute(oldElement, newElement, "checked", ctx); - syncBooleanAttribute(oldElement, newElement, "disabled", ctx); - - if (!newElement.hasAttribute("value")) { - if (!ignoreAttribute("value", oldElement, "remove", ctx)) { - oldElement.value = ""; - oldElement.removeAttribute("value"); - } - } else if (oldValue !== newValue) { - if (!ignoreAttribute("value", oldElement, "update", ctx)) { - oldElement.setAttribute("value", newValue); - oldElement.value = newValue; - } - } - // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why? - // did I break something? - } else if ( - oldElement instanceof HTMLOptionElement && - newElement instanceof HTMLOptionElement - ) { - syncBooleanAttribute(oldElement, newElement, "selected", ctx); - } else if ( - oldElement instanceof HTMLTextAreaElement && - newElement instanceof HTMLTextAreaElement - ) { - let newValue = newElement.value; - let oldValue = oldElement.value; - if (ignoreAttribute("value", oldElement, "update", ctx)) { - return; - } - if (newValue !== oldValue) { - oldElement.value = newValue; - } - if ( - oldElement.firstChild && - oldElement.firstChild.nodeValue !== newValue - ) { - oldElement.firstChild.nodeValue = newValue; - } - } - } - - /** - * @param {Element} oldElement element to sync the value to - * @param {Element} newElement element to sync the value from - * @param {string} attributeName the attribute name - * @param {MorphContext} ctx the merge context - */ - function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - const newLiveValue = newElement[attributeName], - // @ts-ignore ditto - oldLiveValue = oldElement[attributeName]; - if (newLiveValue !== oldLiveValue) { - const ignoreUpdate = ignoreAttribute( - attributeName, - oldElement, - "update", - ctx, - ); - if (!ignoreUpdate) { - // update attribute's associated DOM property - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - oldElement[attributeName] = newElement[attributeName]; - } - if (newLiveValue) { - if (!ignoreUpdate) { - // TODO: do we really want this? tests say so but it feels wrong - oldElement.setAttribute(attributeName, newLiveValue); - } - } else { - if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { - oldElement.removeAttribute(attributeName); - } - } - } - } - /** * ============================================================================= * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style From 1510bdd4f49d0c850b0be124265131d680d33bca Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Mon, 13 Jan 2025 23:57:44 -0600 Subject: [PATCH 16/50] move a few minor things around. --- src/idiomorph.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index fd899e6..6e5cd35 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -115,12 +115,7 @@ var Idiomorph = (function () { // AND NOW IT BEGINS... //============================================================================= - /** - * - * @type {Set} - */ - let EMPTY_SET = new Set(); - + function noOp() {} /** * Default configuration values, updatable by users now * @type {ConfigInternal} @@ -733,8 +728,6 @@ var Idiomorph = (function () { // Misc //============================================================================= - function noOp() {} - const createMorphContext = (function () { /** * @@ -1060,6 +1053,9 @@ var Idiomorph = (function () { return null; } + //============================================================================= + // HTML Normalization Functions + //============================================================================= const normalizeContent = (function() { /** @type {WeakSet} */ const generatedByIdiomorph = new WeakSet(); @@ -1150,6 +1146,10 @@ var Idiomorph = (function () { return normalizeContent; })(); + //============================================================================= + // DOM Manipulation Functions + //============================================================================= + /** * * @param {Node} node @@ -1215,6 +1215,12 @@ var Idiomorph = (function () { // ID Set Functions //============================================================================= + /** + * + * @type {Set} + */ + let EMPTY_SET = new Set(); + /** * * @param {MorphContext} ctx From 74abe689571398debcc4ca5b2042130018d6b394 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 01:04:32 -0600 Subject: [PATCH 17/50] refactor morph entry point. --- src/idiomorph.js | 156 ++++++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 75 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 6e5cd35..12887c3 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -146,7 +146,7 @@ var Idiomorph = (function () { /** * ============================================================================= - * Core Morphing Algorithm - morph, morphNormalizedContent, morphNode, morphChildren + * Core Morphing Algorithm - morph, morphChildren, morphNode * ============================================================================= * * @param {Element | Document} oldNode @@ -155,67 +155,61 @@ var Idiomorph = (function () { * @returns {undefined | Node[]} */ function morph(oldNode, newContent, config = {}) { - if (oldNode instanceof Document) { - oldNode = oldNode.documentElement; - } - - let normalizedContent = normalizeContent(newContent); - - let ctx = createMorphContext(oldNode, normalizedContent, config); - - return morphNormalizedContent(oldNode, normalizedContent, ctx); + oldNode = normalizeElement(oldNode); + const newNode = normalizeParent(newContent); + const ctx = createMorphContext(oldNode, newNode, config); + + return withHeadBlocking(ctx, oldNode, newNode, (ctx) => { + if (ctx.morphStyle === "innerHTML") { + morphChildren(oldNode, newNode, ctx); + ctx.pantry.remove(); + return Array.from(oldNode.childNodes); + } else { + // outerHTML + const oldNodeParent = normalizeParent(oldNode); + const startPoint = oldNode.previousSibling; + const endPoint = oldNode.nextSibling; + morphChildren(oldNodeParent, newNode, ctx, oldNode); + ctx.pantry.remove(); + + let added = []; + let nextSibling = + startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; + while (nextSibling != null && nextSibling != endPoint) { + added.push(nextSibling); + nextSibling = nextSibling.nextSibling; + } + return added; + } + }); } /** - * - * @param {Element} oldNode - * @param {Element} normalizedNewContent * @param {MorphContext} ctx + * @param {Element} oldNode + * @param {Element} newNode * @returns {undefined | Node[]} */ - function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { + function withHeadBlocking(ctx, oldNode, newNode, callback) { if (ctx.head.block) { - let oldHead = oldNode.querySelector("head"); - let newHead = normalizedNewContent.querySelector("head"); + const oldHead = oldNode.querySelector("head"); + const newHead = newNode.querySelector("head"); if (oldHead && newHead) { - let promises = handleHeadElement(oldHead, newHead, ctx); - // when head promises resolve, call morph again, ignoring the head tag - Promise.all(promises).then(function () { - morphNormalizedContent( - oldNode, - normalizedNewContent, - Object.assign(ctx, { - head: { - block: false, - ignore: true, - }, - }), - ); + const promises = handleHeadElement(oldHead, newHead, ctx); + // when head promises resolve, proceed ignoring the head tag + return Promise.all(promises).then(() => { + const newCtx = Object.assign(ctx, { + head: { + block: false, + ignore: true, + }, + }); + return callback(newCtx); }); - return; } } - if (ctx.morphStyle === "innerHTML") { - morphChildren(oldNode, normalizedNewContent, ctx); - ctx.pantry.remove(); - return Array.from(oldNode.children); - } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { - const normalizedOldNode = normalizeContent(oldNode); - const startPoint = oldNode.previousSibling; - const endPoint = oldNode.nextSibling; - morphChildren(normalizedOldNode, normalizedNewContent, ctx, oldNode); - ctx.pantry.remove(); - let added = []; - let nextSibling = - startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; - while (nextSibling != null && nextSibling != endPoint) { - added.push(nextSibling); - nextSibling = nextSibling.nextSibling; - } - return added; - } else { - throw "Do not understand how to morph style " + ctx.morphStyle; - } + // just proceed if we not head blocking + return callback(ctx); } /** @@ -255,7 +249,7 @@ var Idiomorph = (function () { //============================================================================= // Single Node Morphing Code //============================================================================= - const morphNode = (function() { + const morphNode = (function () { /** * @param {Node} oldNode root node to merge content into * @param {Node} newContent new content to merge @@ -465,7 +459,8 @@ var Idiomorph = (function () { return true; } return ( - ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false + ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === + false ); } @@ -621,21 +616,13 @@ var Idiomorph = (function () { * @returns {Promise[]} */ function handleHeadElement(oldHead, newHead, ctx) { - /** - * @type {Node[]} - */ + /** @type {Node[]} */ let added = []; - /** - * @type {Element[]} - */ + /** @type {Element[]} */ let removed = []; - /** - * @type {Element[]} - */ + /** @type {Element[]} */ let preserved = []; - /** - * @type {Element[]} - */ + /** @type {Element[]} */ let nodesToAppend = []; let headMergeStyle = ctx.head.style; @@ -725,7 +712,7 @@ var Idiomorph = (function () { } //============================================================================= - // Misc + // Create Morph Context Functions //============================================================================= const createMorphContext = (function () { @@ -739,11 +726,17 @@ var Idiomorph = (function () { function createMorphContext(oldNode, newContent, config) { const mergedConfig = mergeDefaults(config); const { persistentIds, idMap } = createIdMaps(oldNode, newContent); + + const morphStyle = mergedConfig.morphStyle || "outerHTML"; + if (!["innerHTML", "outerHTML"].includes(morphStyle)) { + throw `Do not understand how to morph style ${morphStyle}`; + } + return { target: oldNode, newContent: newContent, config: mergedConfig, - morphStyle: mergedConfig.morphStyle, + morphStyle: morphStyle, ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, idMap: idMap, @@ -1056,7 +1049,7 @@ var Idiomorph = (function () { //============================================================================= // HTML Normalization Functions //============================================================================= - const normalizeContent = (function() { + const { normalizeElement, normalizeParent } = (function () { /** @type {WeakSet} */ const generatedByIdiomorph = new WeakSet(); @@ -1065,14 +1058,27 @@ var Idiomorph = (function () { * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent * @returns {Element} */ - function normalizeContent(newContent) { + function normalizeElement(content) { + if (content instanceof Document) { + return content.documentElement; + } else { + return content; + } + } + + /** + * + * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent + * @returns {Element} + */ + function normalizeParent(newContent) { if (newContent == null) { - // noinspection UnnecessaryLocalVariableJS - const dummyParent = document.createElement("div"); - return dummyParent; + return document.createElement("div"); // dummy parent element } else if (typeof newContent === "string") { - return normalizeContent(parseContent(newContent)); - } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) { + return normalizeParent(parseContent(newContent)); + } else if ( + generatedByIdiomorph.has(/** @type {Element} */ (newContent)) + ) { // the template tag created by idiomorph parsing can serve as a dummy parent return /** @type {Element} */ (newContent); } else if (newContent instanceof Node) { @@ -1143,7 +1149,7 @@ var Idiomorph = (function () { } } - return normalizeContent; + return { normalizeElement, normalizeParent }; })(); //============================================================================= From 2e07c8c6c3a5c393d380d880665079a2a70399c9 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 01:26:34 -0600 Subject: [PATCH 18/50] isolate morphChildren and its subfunctions. --- src/idiomorph.js | 1232 +++++++++++++++++++++++----------------------- 1 file changed, 620 insertions(+), 612 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 12887c3..6dd0eed 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -184,6 +184,447 @@ var Idiomorph = (function () { }); } + const morphChildren = (function () { + /** + * This is the core algorithm for matching up children. The idea is to use id sets to try to match up + * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but + * by using id sets, we are able to better match up with content deeper in the DOM. + * + * Basic algorithm is, for each node in the new content: + * + * - if we have not reached the end of the old parent: + * - if the new content has an id set match with the current insertion point, morph + * - search for an id set match + * - if id set match found, morph + * - if the new content is a soft match with the current insertion point, morph + * - otherwise search for a "soft" match + * - if a soft match is found, morph + * - otherwise, prepend the new node before the current insertion point + * + * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved + * with the current node. See findIdSetMatch and findSoftMatch for details. + * + * @param {Element} oldParent the old content that we are merging the new content into + * @param {Element} newParent the parent element of the new content + * @param {MorphContext} ctx the merge context + * @param {Element} [onlyNode] + * @returns {void} + */ + function morphChildren(oldParent, newParent, ctx, onlyNode) { + if ( + oldParent instanceof HTMLTemplateElement && + newParent instanceof HTMLTemplateElement + ) { + // @ts-ignore we can pretend the DocumentFragment is an Element + oldParent = oldParent.content; + // @ts-ignore ditto + newParent = newParent.content; + } + let insertionPoint = /** @type {Node | null} */ ( + onlyNode || oldParent.firstChild + ); + let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); + + // run through all the new content + for (const newChild of newParent.childNodes) { + // once we reach the end of the old parent content skip to the end and insert + if (insertionPoint != null && insertionPoint != endPoint) { + // if the current node has an id set match then morph + if (isIdSetMatch(insertionPoint, newChild, ctx)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // otherwise search forward in the existing old children for an id set match + const idSetMatch = findIdSetMatch( + newChild, + insertionPoint, + endPoint, + ctx, + ); + if (idSetMatch) { + insertionPoint = morphChild( + idSetMatch, + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // if the current point is already a soft match morph + if (isSoftMatch(insertionPoint, newChild)) { + insertionPoint = morphChild( + insertionPoint, + newChild, + insertionPoint, + ctx, + ); + continue; + } + + // search forward in the existing old children for a soft match for the current node + const softMatch = findSoftMatch( + newChild, + insertionPoint, + endPoint, + ctx, + ); + if (softMatch) { + insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); + continue; + } + } + // last resort, insert a new node from scratch or reuse and morph a remote node with matching id + insertOrMorphNode(oldParent, newChild, insertionPoint, ctx); + } + + // remove any remaining old nodes that didn't match up with new content + while (insertionPoint && insertionPoint != endPoint) { + const tempNode = insertionPoint; + insertionPoint = insertionPoint.nextSibling; + removeNode(tempNode, ctx); + } + } + + /** + * @param {Node} oldNode the node to be morphed + * @param {Node} newChild the new content + * @param {Node|null} insertionPoint the current point in the DOM we are morphing content at + * @param {MorphContext} ctx the merge context + * @returns {Node|null} returns the new insertion point after the merged node + */ + function morphChild(oldNode, newChild, insertionPoint, ctx) { + // if the node to morph is not at the insertion point then we need to move it here by moving or removing nodes + if (oldNode !== insertionPoint) { + // @ts-ignore we know the Node has a valid parent + moveBefore(oldNode.parentElement, oldNode, insertionPoint); + } + morphNode(oldNode, newChild, ctx); + return oldNode.nextSibling; + } + + /** + * @param {Element | null} oldParent + * @param {Node} newChild new content to merge + * @param {Node | null} insertionPoint insertion point to place content before + * @param {MorphContext} ctx the merge context + */ + function insertOrMorphNode(oldParent, newChild, insertionPoint, ctx) { + if (oldParent == null) return; + if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { + // this node id is somewhere so move it and all its children here and then morph + const movedChild = moveBeforeById( + oldParent, + /** @type {Element} */ (newChild).id, + insertionPoint, + ctx, + ); + morphNode(movedChild, newChild, ctx); + } else { + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { + // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm + const newEmptyChild = document.createElement(newChild.tagName); + oldParent.insertBefore(newEmptyChild, insertionPoint); + morphNode(newEmptyChild, newChild, ctx); + ctx.callbacks.afterNodeAdded(newEmptyChild); + } else { + // no id state to preserve so just insert a clone of the new data to avoid mutating newParent + const newClonedChild = document.importNode(newChild, true); + oldParent.insertBefore(newClonedChild, insertionPoint); + ctx.callbacks.afterNodeAdded(newClonedChild); + } + } + } + + /** + * + * @param {Node} oldNode + * @param {Node} newNode + * @param {MorphContext} ctx + * @returns {boolean} + */ + function isIdSetMatch(oldNode, newNode, ctx) { + if ( + oldNode instanceof Element && + newNode instanceof Element && + oldNode.tagName === newNode.tagName + ) { + if (oldNode.id !== "" && oldNode.id === newNode.id) { + return true; + } else { + return getIdIntersectionCount(oldNode, newNode, ctx) > 0; + } + } + return false; + } + + /** + * + * @param {Node | null} oldNode + * @param {Node | null} newNode + * @returns {boolean} + */ + function isSoftMatch(oldNode, newNode) { + if (oldNode == null || newNode == null) { + return false; + } + // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that + // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing + if ( + /** @type {Element} */ (oldNode).id && + /** @type {Element} */ (oldNode).id !== + /** @type {Element} */ (newNode).id + ) { + return false; + } + return ( + oldNode.nodeType === newNode.nodeType && + /** @type {Element} */ (oldNode).tagName === + /** @type {Element} */ (newNode).tagName + ); + } + + /** + * ============================================================================= + * Scans forward from the insertionPoint in the old parent looking for a potential id match + * for the newChild. We stop if we find a potential id match for the new child OR + * if the number of potential id matches we are discarding is greater than the + * potential id matches for the new child + * ============================================================================= + * @param {Node} newChild + * @param {Node | null} insertionPoint + * @param {Node | null} endPoint + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { + // max id matches we are willing to discard in our search + let newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); + + /** + * @type {Node | null} + */ + + // only search forward if there is a possibility of an id match + if (newChildPotentialIdCount > 0) { + let potentialMatch = insertionPoint; + // if there is a possibility of an id match, scan forward + // keep track of the potential id match count we are discarding (the + // newChildPotentialIdCount must be greater than this to make it likely + // worth it) + let otherMatchCount = 0; + while (potentialMatch != null && potentialMatch != endPoint) { + // If we have an id match, return the current potential match + if (isIdSetMatch(potentialMatch, newChild, ctx)) { + return potentialMatch; + } + + // computer the other potential matches of this new content + otherMatchCount += getPersistentIdNodeCount(ctx, potentialMatch); + + if (otherMatchCount > newChildPotentialIdCount) { + // if we have more potential id matches in _other_ content, we + // do not have a good candidate for an id match, so return null + return null; + } + + // advanced to the next old content child + potentialMatch = potentialMatch.nextSibling; + } + } + return null; + } + + /** + * ============================================================================= + * Scans forward from the insertionPoint in the old parent looking for a potential soft match + * for the newChild. We stop if we find a potential soft match for the new child OR + * if we find a potential id match in the old parents children OR if we find two + * potential soft matches for the next two pieces of new content + * ============================================================================= + * @param {Node} newChild + * @param {Node | null} insertionPoint + * @param {Node | null} endPoint + * @param {MorphContext} ctx + * @returns {null | Node} + */ + function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { + /** + * @type {Node | null} + */ + let potentialSoftMatch = insertionPoint; + /** + * @type {Node | null} + */ + let nextSibling = newChild.nextSibling; + let siblingSoftMatchCount = 0; + + while (potentialSoftMatch != null && potentialSoftMatch != endPoint) { + if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { + // the current potential soft match has a potential id set match with the remaining new + // content so bail out of looking + return null; + } + + // if we have a soft match with the current node, return it + if (isSoftMatch(potentialSoftMatch, newChild)) { + return potentialSoftMatch; + } + + if (isSoftMatch(potentialSoftMatch, nextSibling)) { + // the next new node has a soft match with this node, so + // increment the count of future soft matches + siblingSoftMatchCount++; + // ok to cast: if it was null it couldn't be a soft match + nextSibling = /** @type {Node} */ (nextSibling).nextSibling; + + // If there are two future soft matches, bail to allow the siblings to soft match + // so that we don't consume future soft matches for the sake of the current node + if (siblingSoftMatchCount >= 2) { + return null; + } + } + // advanced to the next old content child + potentialSoftMatch = potentialSoftMatch.nextSibling; + } + + return null; + } + + //============================================================================= + // DOM Manipulation Functions + //============================================================================= + + /** + * + * @param {Node} node + * @param {MorphContext} ctx + */ + function removeNode(node, ctx) { + // skip remove callbacks when we're going to be restoring this from the pantry later + if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { + moveBefore(ctx.pantry, node, null); + } else { + if (ctx.callbacks.beforeNodeRemoved(node) === false) return; + node.parentNode?.removeChild(node); + ctx.callbacks.afterNodeRemoved(node); + } + } + + /** + * Search for an element by id within the document and pantry, and move it using moveBefore. + * + * @param {Element} parentNode - The parent node to which the element will be moved. + * @param {string} id - The ID of the element to be moved. + * @param {Node | null} after - The reference node to insert the element before. + * If `null`, the element is appended as the last child. + * @param {MorphContext} ctx + * @returns {Element} The found element + */ + function moveBeforeById(parentNode, id, after, ctx) { + const target = + /** @type {Element} - will always be found */ + ( + ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`) + ); + moveBefore(parentNode, target, after); + return target; + } + + /** + * Moves an element before another element within the same parent. + * Uses the proposed `moveBefore` API if available, otherwise falls back to `insertBefore`. + * This is essentialy a forward-compat wrapper. + * + * @param {Element} parentNode - The parent node containing the after element. + * @param {Node} element - The element to be moved. + * @param {Node | null} after - The reference node to insert `element` before. + * If `null`, `element` is appended as the last child. + */ + function moveBefore(parentNode, element, after) { + // @ts-ignore - use proposed moveBefore feature + if (parentNode.moveBefore) { + try { + // @ts-ignore - use proposed moveBefore feature + parentNode.moveBefore(element, after); + } catch (e) { + // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry + parentNode.insertBefore(element, after); + } + } else { + parentNode.insertBefore(element, after); + } + } + + //============================================================================= + // ID Set Functions + //============================================================================= + + /** + * + * @type {Set} + */ + let EMPTY_SET = new Set(); + + /** + * + * @param {MorphContext} ctx + * @param {Node} node + * @returns {number} + */ + function getPersistentIdNodeCount(ctx, node) { + let idSet = ctx.idMap.get(node) || EMPTY_SET; + return idSet.size; + } + + /** + * + * @param {MorphContext} ctx + * @param {Node} node + * @returns {boolean} + */ + function hasPersistentIdNodes(ctx, node) { + return getPersistentIdNodeCount(ctx, node) > 0; + } + + /** + * + * @param {Node} oldNode + * @param {Node} newNode + * @param {MorphContext} ctx + * @returns {number} + */ + function getIdIntersectionCount(oldNode, newNode, ctx) { + let oldSet = ctx.idMap.get(oldNode) || EMPTY_SET; + let newSet = ctx.idMap.get(newNode) || EMPTY_SET; + + let matchCount = 0; + for (const id of oldSet) { + // a potential match is an id in the new and old nodes that + // has not already been merged into the DOM + // But the newNode content we call this on has not been + // merged yet and we don't allow duplicate IDs so it is simple + if (newSet.has(id)) { + ++matchCount; + } + } + return matchCount; + } + + return morphChildren; + })(); + + //============================================================================= + // Head Management Functions + //============================================================================= + /** * @param {MorphContext} ctx * @param {Element} oldNode @@ -213,37 +654,111 @@ var Idiomorph = (function () { } /** - * @param {Element | null} oldParent - * @param {Node} newChild new content to merge - * @param {Node | null} insertionPoint insertion point to place content before - * @param {MorphContext} ctx the merge context + * ============================================================================= + * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style + * ============================================================================= + * @param {Element} oldHead + * @param {Element} newHead + * @param {MorphContext} ctx + * @returns {Promise[]} */ - function insertOrMorphNode(oldParent, newChild, insertionPoint, ctx) { - if (oldParent == null) return; - if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { - // this node id is somewhere so move it and all its children here and then morph - const movedChild = moveBeforeById( - oldParent, - /** @type {Element} */ (newChild).id, - insertionPoint, - ctx, - ); - morphNode(movedChild, newChild, ctx); - } else { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; - if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { - // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm - const newEmptyChild = document.createElement(newChild.tagName); - oldParent.insertBefore(newEmptyChild, insertionPoint); - morphNode(newEmptyChild, newChild, ctx); - ctx.callbacks.afterNodeAdded(newEmptyChild); + function handleHeadElement(oldHead, newHead, ctx) { + /** @type {Node[]} */ + let added = []; + /** @type {Element[]} */ + let removed = []; + /** @type {Element[]} */ + let preserved = []; + /** @type {Element[]} */ + let nodesToAppend = []; + + let headMergeStyle = ctx.head.style; + + // put all new head elements into a Map, by their outerHTML + let srcToNewHeadNodes = new Map(); + for (const newHeadChild of newHead.children) { + srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + } + + // for each elt in the current head + for (const currentHeadElt of oldHead.children) { + // If the current head element is in the map + let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); + let isReAppended = ctx.head.shouldReAppend(currentHeadElt); + let isPreserved = ctx.head.shouldPreserve(currentHeadElt); + if (inNewContent || isPreserved) { + if (isReAppended) { + // remove the current version and let the new version replace it and re-execute + removed.push(currentHeadElt); + } else { + // this element already exists and should not be re-appended, so remove it from + // the new content map, preserving it in the DOM + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); + preserved.push(currentHeadElt); + } } else { - // no id state to preserve so just insert a clone of the new data to avoid mutating newParent - const newClonedChild = document.importNode(newChild, true); - oldParent.insertBefore(newClonedChild, insertionPoint); - ctx.callbacks.afterNodeAdded(newClonedChild); + if (headMergeStyle === "append") { + // we are appending and this existing element is not new content + // so if and only if it is marked for re-append do we do anything + if (isReAppended) { + removed.push(currentHeadElt); + nodesToAppend.push(currentHeadElt); + } + } else { + // if this is a merge, we remove this content since it is not in the new head + if (ctx.head.shouldRemove(currentHeadElt) !== false) { + removed.push(currentHeadElt); + } + } } } + + // Push the remaining new head elements in the Map into the + // nodes to append to the head tag + nodesToAppend.push(...srcToNewHeadNodes.values()); + + let promises = []; + for (const newNode of nodesToAppend) { + // TODO: This could theoretically be null, based on type + let newElt = /** @type {ChildNode} */ ( + document.createRange().createContextualFragment(newNode.outerHTML) + .firstChild + ); + if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { + if ( + ("href" in newElt && newElt.href) || + ("src" in newElt && newElt.src) + ) { + /** @type {(result?: any) => void} */ let resolve; + let promise = new Promise(function (_resolve) { + resolve = _resolve; + }); + newElt.addEventListener("load", function () { + resolve(); + }); + promises.push(promise); + } + oldHead.appendChild(newElt); + ctx.callbacks.afterNodeAdded(newElt); + added.push(newElt); + } + } + + // remove all removed elements, after we have appended the new elements to avoid + // additional network requests for things like style sheets + for (const removedElement of removed) { + if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { + oldHead.removeChild(removedElement); + ctx.callbacks.afterNodeRemoved(removedElement); + } + } + + ctx.head.afterHeadMorphed(oldHead, { + added: added, + kept: preserved, + removed: removed, + }); + return promises; } //============================================================================= @@ -386,335 +901,103 @@ var Idiomorph = (function () { newElement instanceof HTMLOptionElement ) { syncBooleanAttribute(oldElement, newElement, "selected", ctx); - } else if ( - oldElement instanceof HTMLTextAreaElement && - newElement instanceof HTMLTextAreaElement - ) { - let newValue = newElement.value; - let oldValue = oldElement.value; - if (ignoreAttribute("value", oldElement, "update", ctx)) { - return; - } - if (newValue !== oldValue) { - oldElement.value = newValue; - } - if ( - oldElement.firstChild && - oldElement.firstChild.nodeValue !== newValue - ) { - oldElement.firstChild.nodeValue = newValue; - } - } - } - - /** - * @param {Element} oldElement element to sync the value to - * @param {Element} newElement element to sync the value from - * @param {string} attributeName the attribute name - * @param {MorphContext} ctx the merge context - */ - function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - const newLiveValue = newElement[attributeName], - // @ts-ignore ditto - oldLiveValue = oldElement[attributeName]; - if (newLiveValue !== oldLiveValue) { - const ignoreUpdate = ignoreAttribute( - attributeName, - oldElement, - "update", - ctx, - ); - if (!ignoreUpdate) { - // update attribute's associated DOM property - // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties - oldElement[attributeName] = newElement[attributeName]; - } - if (newLiveValue) { - if (!ignoreUpdate) { - // TODO: do we really want this? tests say so but it feels wrong - oldElement.setAttribute(attributeName, newLiveValue); - } - } else { - if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { - oldElement.removeAttribute(attributeName); - } - } - } - } - - /** - * @param {string} attr the attribute to be mutated - * @param {Element} element the element that is going to be updated - * @param {"update" | "remove"} updateType - * @param {MorphContext} ctx the merge context - * @returns {boolean} true if the attribute should be ignored, false otherwise - */ - function ignoreAttribute(attr, element, updateType, ctx) { - if ( - attr === "value" && - ctx.ignoreActiveValue && - element === document.activeElement - ) { - return true; - } - return ( - ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === - false - ); - } - - /** - * @param {Node} possibleActiveElement - * @param {MorphContext} ctx - * @returns {boolean} - */ - function ignoreValueOfActiveElement(possibleActiveElement, ctx) { - return ( - !!ctx.ignoreActiveValue && - possibleActiveElement === document.activeElement && - possibleActiveElement !== document.body - ); - } - - return morphNode; - })(); - - /** - * @param {Node} oldNode the node to be morphed - * @param {Node} newChild the new content - * @param {Node|null} insertionPoint the current point in the DOM we are morphing content at - * @param {MorphContext} ctx the merge context - * @returns {Node|null} returns the new insertion point after the merged node - */ - function morphChild(oldNode, newChild, insertionPoint, ctx) { - // if the node to morph is not at the insertion point then we need to move it here by moving or removing nodes - if (oldNode !== insertionPoint) { - // @ts-ignore we know the Node has a valid parent - moveBefore(oldNode.parentElement, oldNode, insertionPoint); - } - morphNode(oldNode, newChild, ctx); - return oldNode.nextSibling; - } - - /** - * This is the core algorithm for matching up children. The idea is to use id sets to try to match up - * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but - * by using id sets, we are able to better match up with content deeper in the DOM. - * - * Basic algorithm is, for each node in the new content: - * - * - if we have not reached the end of the old parent: - * - if the new content has an id set match with the current insertion point, morph - * - search for an id set match - * - if id set match found, morph - * - if the new content is a soft match with the current insertion point, morph - * - otherwise search for a "soft" match - * - if a soft match is found, morph - * - otherwise, prepend the new node before the current insertion point - * - * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved - * with the current node. See findIdSetMatch and findSoftMatch for details. - * - * @param {Element} oldParent the old content that we are merging the new content into - * @param {Element} newParent the parent element of the new content - * @param {MorphContext} ctx the merge context - * @param {Element} [onlyNode] - * @returns {void} - */ - function morphChildren(oldParent, newParent, ctx, onlyNode) { - if ( - oldParent instanceof HTMLTemplateElement && - newParent instanceof HTMLTemplateElement - ) { - // @ts-ignore we can pretend the DocumentFragment is an Element - oldParent = oldParent.content; - // @ts-ignore ditto - newParent = newParent.content; - } - let insertionPoint = /** @type {Node | null} */ ( - onlyNode || oldParent.firstChild - ); - let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); - - // run through all the new content - for (const newChild of newParent.childNodes) { - // once we reach the end of the old parent content skip to the end and insert - if (insertionPoint != null && insertionPoint != endPoint) { - // if the current node has an id set match then morph - if (isIdSetMatch(insertionPoint, newChild, ctx)) { - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - continue; - } - - // otherwise search forward in the existing old children for an id set match - const idSetMatch = findIdSetMatch( - newChild, - insertionPoint, - endPoint, - ctx, - ); - if (idSetMatch) { - //insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); - insertionPoint = morphChild( - idSetMatch, - newChild, - insertionPoint, - ctx, - ); - continue; - } - - // if the current point is already a soft match morph - if (isSoftMatch(insertionPoint, newChild)) { - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - continue; - } - - // search forward in the existing old children for a soft match for the current node - const softMatch = findSoftMatch( - newChild, - insertionPoint, - endPoint, - ctx, - ); - if (softMatch) { - //insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); - insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); - continue; - } - } - // last resort, insert a new node from scratch or reuse and morph a remote node with matching id - insertOrMorphNode(oldParent, newChild, insertionPoint, ctx); - } - - // remove any remaining old nodes that didn't match up with new content - while (insertionPoint && insertionPoint != endPoint) { - const tempNode = insertionPoint; - insertionPoint = insertionPoint.nextSibling; - removeNode(tempNode, ctx); - } - } - - /** - * ============================================================================= - * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style - * ============================================================================= - * @param {Element} oldHead - * @param {Element} newHead - * @param {MorphContext} ctx - * @returns {Promise[]} - */ - function handleHeadElement(oldHead, newHead, ctx) { - /** @type {Node[]} */ - let added = []; - /** @type {Element[]} */ - let removed = []; - /** @type {Element[]} */ - let preserved = []; - /** @type {Element[]} */ - let nodesToAppend = []; - - let headMergeStyle = ctx.head.style; - - // put all new head elements into a Map, by their outerHTML - let srcToNewHeadNodes = new Map(); - for (const newHeadChild of newHead.children) { - srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + } else if ( + oldElement instanceof HTMLTextAreaElement && + newElement instanceof HTMLTextAreaElement + ) { + let newValue = newElement.value; + let oldValue = oldElement.value; + if (ignoreAttribute("value", oldElement, "update", ctx)) { + return; + } + if (newValue !== oldValue) { + oldElement.value = newValue; + } + if ( + oldElement.firstChild && + oldElement.firstChild.nodeValue !== newValue + ) { + oldElement.firstChild.nodeValue = newValue; + } + } } - // for each elt in the current head - for (const currentHeadElt of oldHead.children) { - // If the current head element is in the map - let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); - let isReAppended = ctx.head.shouldReAppend(currentHeadElt); - let isPreserved = ctx.head.shouldPreserve(currentHeadElt); - if (inNewContent || isPreserved) { - if (isReAppended) { - // remove the current version and let the new version replace it and re-execute - removed.push(currentHeadElt); - } else { - // this element already exists and should not be re-appended, so remove it from - // the new content map, preserving it in the DOM - srcToNewHeadNodes.delete(currentHeadElt.outerHTML); - preserved.push(currentHeadElt); + /** + * @param {Element} oldElement element to sync the value to + * @param {Element} newElement element to sync the value from + * @param {string} attributeName the attribute name + * @param {MorphContext} ctx the merge context + */ + function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) { + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + const newLiveValue = newElement[attributeName], + // @ts-ignore ditto + oldLiveValue = oldElement[attributeName]; + if (newLiveValue !== oldLiveValue) { + const ignoreUpdate = ignoreAttribute( + attributeName, + oldElement, + "update", + ctx, + ); + if (!ignoreUpdate) { + // update attribute's associated DOM property + // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties + oldElement[attributeName] = newElement[attributeName]; } - } else { - if (headMergeStyle === "append") { - // we are appending and this existing element is not new content - // so if and only if it is marked for re-append do we do anything - if (isReAppended) { - removed.push(currentHeadElt); - nodesToAppend.push(currentHeadElt); + if (newLiveValue) { + if (!ignoreUpdate) { + // TODO: do we really want this? tests say so but it feels wrong + oldElement.setAttribute(attributeName, newLiveValue); } } else { - // if this is a merge, we remove this content since it is not in the new head - if (ctx.head.shouldRemove(currentHeadElt) !== false) { - removed.push(currentHeadElt); + if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { + oldElement.removeAttribute(attributeName); } } } } - // Push the remaining new head elements in the Map into the - // nodes to append to the head tag - nodesToAppend.push(...srcToNewHeadNodes.values()); - - let promises = []; - for (const newNode of nodesToAppend) { - // TODO: This could theoretically be null, based on type - let newElt = /** @type {ChildNode} */ ( - document.createRange().createContextualFragment(newNode.outerHTML) - .firstChild - ); - if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { - if ( - ("href" in newElt && newElt.href) || - ("src" in newElt && newElt.src) - ) { - /** @type {(result?: any) => void} */ let resolve; - let promise = new Promise(function (_resolve) { - resolve = _resolve; - }); - newElt.addEventListener("load", function () { - resolve(); - }); - promises.push(promise); - } - oldHead.appendChild(newElt); - ctx.callbacks.afterNodeAdded(newElt); - added.push(newElt); + /** + * @param {string} attr the attribute to be mutated + * @param {Element} element the element that is going to be updated + * @param {"update" | "remove"} updateType + * @param {MorphContext} ctx the merge context + * @returns {boolean} true if the attribute should be ignored, false otherwise + */ + function ignoreAttribute(attr, element, updateType, ctx) { + if ( + attr === "value" && + ctx.ignoreActiveValue && + element === document.activeElement + ) { + return true; } + return ( + ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === + false + ); } - // remove all removed elements, after we have appended the new elements to avoid - // additional network requests for things like style sheets - for (const removedElement of removed) { - removeNode(removedElement, ctx); + /** + * @param {Node} possibleActiveElement + * @param {MorphContext} ctx + * @returns {boolean} + */ + function ignoreValueOfActiveElement(possibleActiveElement, ctx) { + return ( + !!ctx.ignoreActiveValue && + possibleActiveElement === document.activeElement && + possibleActiveElement !== document.body + ); } - ctx.head.afterHeadMorphed(oldHead, { - added: added, - kept: preserved, - removed: removed, - }); - return promises; - } + return morphNode; + })(); //============================================================================= // Create Morph Context Functions //============================================================================= - const createMorphContext = (function () { /** * @@ -891,161 +1174,6 @@ var Idiomorph = (function () { return createMorphContext; })(); - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {boolean} - */ - function isIdSetMatch(oldNode, newNode, ctx) { - if ( - oldNode instanceof Element && - newNode instanceof Element && - oldNode.tagName === newNode.tagName - ) { - if (oldNode.id !== "" && oldNode.id === newNode.id) { - return true; - } else { - return getIdIntersectionCount(oldNode, newNode, ctx) > 0; - } - } - return false; - } - - /** - * - * @param {Node | null} oldNode - * @param {Node | null} newNode - * @returns {boolean} - */ - function isSoftMatch(oldNode, newNode) { - if (oldNode == null || newNode == null) { - return false; - } - // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that - // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing - if ( - /** @type {Element} */ (oldNode).id && - /** @type {Element} */ (oldNode).id !== - /** @type {Element} */ (newNode).id - ) { - return false; - } - return ( - oldNode.nodeType === newNode.nodeType && - /** @type {Element} */ (oldNode).tagName === - /** @type {Element} */ (newNode).tagName - ); - } - - /** - * ============================================================================= - * Scans forward from the insertionPoint in the old parent looking for a potential id match - * for the newChild. We stop if we find a potential id match for the new child OR - * if the number of potential id matches we are discarding is greater than the - * potential id matches for the new child - * ============================================================================= - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { - // max id matches we are willing to discard in our search - let newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); - - /** - * @type {Node | null} - */ - - // only search forward if there is a possibility of an id match - if (newChildPotentialIdCount > 0) { - let potentialMatch = insertionPoint; - // if there is a possibility of an id match, scan forward - // keep track of the potential id match count we are discarding (the - // newChildPotentialIdCount must be greater than this to make it likely - // worth it) - let otherMatchCount = 0; - while (potentialMatch != null && potentialMatch != endPoint) { - // If we have an id match, return the current potential match - if (isIdSetMatch(potentialMatch, newChild, ctx)) { - return potentialMatch; - } - - // computer the other potential matches of this new content - otherMatchCount += getPersistentIdNodeCount(ctx, potentialMatch); - - if (otherMatchCount > newChildPotentialIdCount) { - // if we have more potential id matches in _other_ content, we - // do not have a good candidate for an id match, so return null - return null; - } - - // advanced to the next old content child - potentialMatch = potentialMatch.nextSibling; - } - } - return null; - } - - /** - * ============================================================================= - * Scans forward from the insertionPoint in the old parent looking for a potential soft match - * for the newChild. We stop if we find a potential soft match for the new child OR - * if we find a potential id match in the old parents children OR if we find two - * potential soft matches for the next two pieces of new content - * ============================================================================= - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {null | Node} - */ - function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { - /** - * @type {Node | null} - */ - let potentialSoftMatch = insertionPoint; - /** - * @type {Node | null} - */ - let nextSibling = newChild.nextSibling; - let siblingSoftMatchCount = 0; - - while (potentialSoftMatch != null && potentialSoftMatch != endPoint) { - if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { - // the current potential soft match has a potential id set match with the remaining new - // content so bail out of looking - return null; - } - - // if we have a soft match with the current node, return it - if (isSoftMatch(potentialSoftMatch, newChild)) { - return potentialSoftMatch; - } - - if (isSoftMatch(potentialSoftMatch, nextSibling)) { - // the next new node has a soft match with this node, so - // increment the count of future soft matches - siblingSoftMatchCount++; - // ok to cast: if it was null it couldn't be a soft match - nextSibling = /** @type {Node} */ (nextSibling).nextSibling; - - // If there are two future soft matches, bail to allow the siblings to soft match - // so that we don't consume future soft matches for the sake of the current node - if (siblingSoftMatchCount >= 2) { - return null; - } - } - // advanced to the next old content child - potentialSoftMatch = potentialSoftMatch.nextSibling; - } - - return null; - } - //============================================================================= // HTML Normalization Functions //============================================================================= @@ -1152,126 +1280,6 @@ var Idiomorph = (function () { return { normalizeElement, normalizeParent }; })(); - //============================================================================= - // DOM Manipulation Functions - //============================================================================= - - /** - * - * @param {Node} node - * @param {MorphContext} ctx - */ - function removeNode(node, ctx) { - // skip remove callbacks when we're going to be restoring this from the pantry later - if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { - moveBefore(ctx.pantry, node, null); - } else { - if (ctx.callbacks.beforeNodeRemoved(node) === false) return; - node.parentNode?.removeChild(node); - ctx.callbacks.afterNodeRemoved(node); - } - } - - /** - * Search for an element by id within the document and pantry, and move it using moveBefore. - * - * @param {Element} parentNode - The parent node to which the element will be moved. - * @param {string} id - The ID of the element to be moved. - * @param {Node | null} after - The reference node to insert the element before. - * If `null`, the element is appended as the last child. - * @param {MorphContext} ctx - * @returns {Element} The found element - */ - function moveBeforeById(parentNode, id, after, ctx) { - const target = - /** @type {Element} - will always be found */ - ( - ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`) - ); - moveBefore(parentNode, target, after); - return target; - } - - /** - * Moves an element before another element within the same parent. - * Uses the proposed `moveBefore` API if available, otherwise falls back to `insertBefore`. - * This is essentialy a forward-compat wrapper. - * - * @param {Element} parentNode - The parent node containing the after element. - * @param {Node} element - The element to be moved. - * @param {Node | null} after - The reference node to insert `element` before. - * If `null`, `element` is appended as the last child. - */ - function moveBefore(parentNode, element, after) { - // @ts-ignore - use proposed moveBefore feature - if (parentNode.moveBefore) { - try { - // @ts-ignore - use proposed moveBefore feature - parentNode.moveBefore(element, after); - } catch (e) { - // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry - parentNode.insertBefore(element, after); - } - } else { - parentNode.insertBefore(element, after); - } - } - - //============================================================================= - // ID Set Functions - //============================================================================= - - /** - * - * @type {Set} - */ - let EMPTY_SET = new Set(); - - /** - * - * @param {MorphContext} ctx - * @param {Node} node - * @returns {number} - */ - function getPersistentIdNodeCount(ctx, node) { - let idSet = ctx.idMap.get(node) || EMPTY_SET; - return idSet.size; - } - - /** - * - * @param {MorphContext} ctx - * @param {Node} node - * @returns {boolean} - */ - function hasPersistentIdNodes(ctx, node) { - return getPersistentIdNodeCount(ctx, node) > 0; - } - - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {number} - */ - function getIdIntersectionCount(oldNode, newNode, ctx) { - let oldSet = ctx.idMap.get(oldNode) || EMPTY_SET; - let newSet = ctx.idMap.get(newNode) || EMPTY_SET; - - let matchCount = 0; - for (const id of oldSet) { - // a potential match is an id in the new and old nodes that - // has not already been merged into the DOM - // But the newNode content we call this on has not been - // merged yet and we don't allow duplicate IDs so it is simple - if (newSet.has(id)) { - ++matchCount; - } - } - return matchCount; - } - //============================================================================= // This is what ends up becoming the Idiomorph global object //============================================================================= From 3a75cc5a4268418a53767a075677d3b3cccbfd4c Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 01:42:14 -0600 Subject: [PATCH 19/50] inline insertOrMorphNode and extract createNode. --- src/idiomorph.js | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 6dd0eed..890fe96 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -276,12 +276,31 @@ var Idiomorph = (function () { ctx, ); if (softMatch) { - insertionPoint = morphChild(softMatch, newChild, insertionPoint, ctx); + insertionPoint = morphChild( + softMatch, + newChild, + insertionPoint, + ctx, + ); continue; } } - // last resort, insert a new node from scratch or reuse and morph a remote node with matching id - insertOrMorphNode(oldParent, newChild, insertionPoint, ctx); + + // if the matching node is in the upcoming old content + if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { + // move it and all its children here and morph + const movedChild = moveBeforeById( + oldParent, + /** @type {Element} */ (newChild).id, + insertionPoint, + ctx, + ); + morphNode(movedChild, newChild, ctx); + continue; + } + + // last resort, insert a new node from scratch + createNode(oldParent, newChild, insertionPoint, ctx); } // remove any remaining old nodes that didn't match up with new content @@ -309,37 +328,19 @@ var Idiomorph = (function () { return oldNode.nextSibling; } - /** - * @param {Element | null} oldParent - * @param {Node} newChild new content to merge - * @param {Node | null} insertionPoint insertion point to place content before - * @param {MorphContext} ctx the merge context - */ - function insertOrMorphNode(oldParent, newChild, insertionPoint, ctx) { - if (oldParent == null) return; - if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { - // this node id is somewhere so move it and all its children here and then morph - const movedChild = moveBeforeById( - oldParent, - /** @type {Element} */ (newChild).id, - insertionPoint, - ctx, - ); - morphNode(movedChild, newChild, ctx); + function createNode(oldParent, newChild, insertionPoint, ctx) { + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { + // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm + const newEmptyChild = document.createElement(newChild.tagName); + oldParent.insertBefore(newEmptyChild, insertionPoint); + morphNode(newEmptyChild, newChild, ctx); + ctx.callbacks.afterNodeAdded(newEmptyChild); } else { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; - if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { - // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm - const newEmptyChild = document.createElement(newChild.tagName); - oldParent.insertBefore(newEmptyChild, insertionPoint); - morphNode(newEmptyChild, newChild, ctx); - ctx.callbacks.afterNodeAdded(newEmptyChild); - } else { - // no id state to preserve so just insert a clone of the new data to avoid mutating newParent - const newClonedChild = document.importNode(newChild, true); - oldParent.insertBefore(newClonedChild, insertionPoint); - ctx.callbacks.afterNodeAdded(newClonedChild); - } + // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants + const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent + oldParent.insertBefore(newClonedChild, insertionPoint); + ctx.callbacks.afterNodeAdded(newClonedChild); } } @@ -532,7 +533,8 @@ var Idiomorph = (function () { const target = /** @type {Element} - will always be found */ ( - ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`) + ctx.target.querySelector(`#${id}`) || + ctx.pantry.querySelector(`#${id}`) ); moveBefore(parentNode, target, after); return target; From 8c23dca609b503574fca29c830de069c9c124ac3 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 01:56:20 -0600 Subject: [PATCH 20/50] final movement. --- src/idiomorph.js | 279 +++++++++++++++++++++++------------------------ 1 file changed, 139 insertions(+), 140 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 890fe96..381b205 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -623,146 +623,6 @@ var Idiomorph = (function () { return morphChildren; })(); - //============================================================================= - // Head Management Functions - //============================================================================= - - /** - * @param {MorphContext} ctx - * @param {Element} oldNode - * @param {Element} newNode - * @returns {undefined | Node[]} - */ - function withHeadBlocking(ctx, oldNode, newNode, callback) { - if (ctx.head.block) { - const oldHead = oldNode.querySelector("head"); - const newHead = newNode.querySelector("head"); - if (oldHead && newHead) { - const promises = handleHeadElement(oldHead, newHead, ctx); - // when head promises resolve, proceed ignoring the head tag - return Promise.all(promises).then(() => { - const newCtx = Object.assign(ctx, { - head: { - block: false, - ignore: true, - }, - }); - return callback(newCtx); - }); - } - } - // just proceed if we not head blocking - return callback(ctx); - } - - /** - * ============================================================================= - * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style - * ============================================================================= - * @param {Element} oldHead - * @param {Element} newHead - * @param {MorphContext} ctx - * @returns {Promise[]} - */ - function handleHeadElement(oldHead, newHead, ctx) { - /** @type {Node[]} */ - let added = []; - /** @type {Element[]} */ - let removed = []; - /** @type {Element[]} */ - let preserved = []; - /** @type {Element[]} */ - let nodesToAppend = []; - - let headMergeStyle = ctx.head.style; - - // put all new head elements into a Map, by their outerHTML - let srcToNewHeadNodes = new Map(); - for (const newHeadChild of newHead.children) { - srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); - } - - // for each elt in the current head - for (const currentHeadElt of oldHead.children) { - // If the current head element is in the map - let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); - let isReAppended = ctx.head.shouldReAppend(currentHeadElt); - let isPreserved = ctx.head.shouldPreserve(currentHeadElt); - if (inNewContent || isPreserved) { - if (isReAppended) { - // remove the current version and let the new version replace it and re-execute - removed.push(currentHeadElt); - } else { - // this element already exists and should not be re-appended, so remove it from - // the new content map, preserving it in the DOM - srcToNewHeadNodes.delete(currentHeadElt.outerHTML); - preserved.push(currentHeadElt); - } - } else { - if (headMergeStyle === "append") { - // we are appending and this existing element is not new content - // so if and only if it is marked for re-append do we do anything - if (isReAppended) { - removed.push(currentHeadElt); - nodesToAppend.push(currentHeadElt); - } - } else { - // if this is a merge, we remove this content since it is not in the new head - if (ctx.head.shouldRemove(currentHeadElt) !== false) { - removed.push(currentHeadElt); - } - } - } - } - - // Push the remaining new head elements in the Map into the - // nodes to append to the head tag - nodesToAppend.push(...srcToNewHeadNodes.values()); - - let promises = []; - for (const newNode of nodesToAppend) { - // TODO: This could theoretically be null, based on type - let newElt = /** @type {ChildNode} */ ( - document.createRange().createContextualFragment(newNode.outerHTML) - .firstChild - ); - if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { - if ( - ("href" in newElt && newElt.href) || - ("src" in newElt && newElt.src) - ) { - /** @type {(result?: any) => void} */ let resolve; - let promise = new Promise(function (_resolve) { - resolve = _resolve; - }); - newElt.addEventListener("load", function () { - resolve(); - }); - promises.push(promise); - } - oldHead.appendChild(newElt); - ctx.callbacks.afterNodeAdded(newElt); - added.push(newElt); - } - } - - // remove all removed elements, after we have appended the new elements to avoid - // additional network requests for things like style sheets - for (const removedElement of removed) { - if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { - oldHead.removeChild(removedElement); - ctx.callbacks.afterNodeRemoved(removedElement); - } - } - - ctx.head.afterHeadMorphed(oldHead, { - added: added, - kept: preserved, - removed: removed, - }); - return promises; - } - //============================================================================= // Single Node Morphing Code //============================================================================= @@ -997,6 +857,145 @@ var Idiomorph = (function () { return morphNode; })(); + //============================================================================= + // Head Management Functions + //============================================================================= + /** + * @param {MorphContext} ctx + * @param {Element} oldNode + * @param {Element} newNode + * @returns {undefined | Node[]} + */ + function withHeadBlocking(ctx, oldNode, newNode, callback) { + if (ctx.head.block) { + const oldHead = oldNode.querySelector("head"); + const newHead = newNode.querySelector("head"); + if (oldHead && newHead) { + const promises = handleHeadElement(oldHead, newHead, ctx); + // when head promises resolve, proceed ignoring the head tag + return Promise.all(promises).then(() => { + const newCtx = Object.assign(ctx, { + head: { + block: false, + ignore: true, + }, + }); + return callback(newCtx); + }); + } + } + // just proceed if we not head blocking + return callback(ctx); + } + + /** + * ============================================================================= + * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style + * ============================================================================= + * @param {Element} oldHead + * @param {Element} newHead + * @param {MorphContext} ctx + * @returns {Promise[]} + */ + function handleHeadElement(oldHead, newHead, ctx) { + /** @type {Node[]} */ + let added = []; + /** @type {Element[]} */ + let removed = []; + /** @type {Element[]} */ + let preserved = []; + /** @type {Element[]} */ + let nodesToAppend = []; + + let headMergeStyle = ctx.head.style; + + // put all new head elements into a Map, by their outerHTML + let srcToNewHeadNodes = new Map(); + for (const newHeadChild of newHead.children) { + srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + } + + // for each elt in the current head + for (const currentHeadElt of oldHead.children) { + // If the current head element is in the map + let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); + let isReAppended = ctx.head.shouldReAppend(currentHeadElt); + let isPreserved = ctx.head.shouldPreserve(currentHeadElt); + if (inNewContent || isPreserved) { + if (isReAppended) { + // remove the current version and let the new version replace it and re-execute + removed.push(currentHeadElt); + } else { + // this element already exists and should not be re-appended, so remove it from + // the new content map, preserving it in the DOM + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); + preserved.push(currentHeadElt); + } + } else { + if (headMergeStyle === "append") { + // we are appending and this existing element is not new content + // so if and only if it is marked for re-append do we do anything + if (isReAppended) { + removed.push(currentHeadElt); + nodesToAppend.push(currentHeadElt); + } + } else { + // if this is a merge, we remove this content since it is not in the new head + if (ctx.head.shouldRemove(currentHeadElt) !== false) { + removed.push(currentHeadElt); + } + } + } + } + + // Push the remaining new head elements in the Map into the + // nodes to append to the head tag + nodesToAppend.push(...srcToNewHeadNodes.values()); + + let promises = []; + for (const newNode of nodesToAppend) { + // TODO: This could theoretically be null, based on type + let newElt = /** @type {ChildNode} */ ( + document.createRange().createContextualFragment(newNode.outerHTML) + .firstChild + ); + if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { + if ( + ("href" in newElt && newElt.href) || + ("src" in newElt && newElt.src) + ) { + /** @type {(result?: any) => void} */ let resolve; + let promise = new Promise(function (_resolve) { + resolve = _resolve; + }); + newElt.addEventListener("load", function () { + resolve(); + }); + promises.push(promise); + } + oldHead.appendChild(newElt); + ctx.callbacks.afterNodeAdded(newElt); + added.push(newElt); + } + } + + // remove all removed elements, after we have appended the new elements to avoid + // additional network requests for things like style sheets + for (const removedElement of removed) { + if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { + oldHead.removeChild(removedElement); + ctx.callbacks.afterNodeRemoved(removedElement); + } + } + + ctx.head.afterHeadMorphed(oldHead, { + added: added, + kept: preserved, + removed: removed, + }); + return promises; + } + //============================================================================= // Create Morph Context Functions //============================================================================= From 0b5eeed91cc736889d5239155519e038d3de620e Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 02:12:16 -0600 Subject: [PATCH 21/50] let morphChildren return the nodes it morphs. --- src/idiomorph.js | 76 +++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 381b205..14f45f6 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -160,27 +160,21 @@ var Idiomorph = (function () { const ctx = createMorphContext(oldNode, newNode, config); return withHeadBlocking(ctx, oldNode, newNode, (ctx) => { + let morphedNodes; if (ctx.morphStyle === "innerHTML") { - morphChildren(oldNode, newNode, ctx); - ctx.pantry.remove(); - return Array.from(oldNode.childNodes); + morphedNodes = morphChildren(ctx, oldNode, newNode); } else { // outerHTML - const oldNodeParent = normalizeParent(oldNode); - const startPoint = oldNode.previousSibling; - const endPoint = oldNode.nextSibling; - morphChildren(oldNodeParent, newNode, ctx, oldNode); - ctx.pantry.remove(); - - let added = []; - let nextSibling = - startPoint?.nextSibling || oldNode.parentNode?.firstChild || null; - while (nextSibling != null && nextSibling != endPoint) { - added.push(nextSibling); - nextSibling = nextSibling.nextSibling; - } - return added; + morphedNodes = morphChildren( + ctx, + normalizeParent(oldNode), + newNode, + oldNode, + oldNode.nextSibling, + ); } + ctx.pantry.remove(); + return morphedNodes; }); } @@ -204,13 +198,20 @@ var Idiomorph = (function () { * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch and findSoftMatch for details. * + * @param {MorphContext} ctx the merge context * @param {Element} oldParent the old content that we are merging the new content into * @param {Element} newParent the parent element of the new content - * @param {MorphContext} ctx the merge context - * @param {Element} [onlyNode] - * @returns {void} + * @param {Element | null} insertionPoint the point in the DOM we start morphing at (defaults to first child) + * @param {Element | null} endPoint the point in the DOM we stop morphing at (defaults to after last child) + * @returns {Node[]} */ - function morphChildren(oldParent, newParent, ctx, onlyNode) { + function morphChildren( + ctx, + oldParent, + newParent, + insertionPoint, + endPoint, + ) { if ( oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement @@ -220,24 +221,22 @@ var Idiomorph = (function () { // @ts-ignore ditto newParent = newParent.content; } - let insertionPoint = /** @type {Node | null} */ ( - onlyNode || oldParent.firstChild - ); - let endPoint = /** @type {Node | null} */ (onlyNode?.nextSibling || null); + insertionPoint ||= oldParent.firstChild; // run through all the new content - for (const newChild of newParent.childNodes) { - // once we reach the end of the old parent content skip to the end and insert - if (insertionPoint != null && insertionPoint != endPoint) { + const morphedNodes = [...newParent.childNodes].map((newChild) => { + // once we reach the end of the old parent content skip to the end and insert the rest + if (insertionPoint != endPoint) { // if the current node has an id set match then morph if (isIdSetMatch(insertionPoint, newChild, ctx)) { + const morphedNode = insertionPoint; insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, ctx, ); - continue; + return morphedNode; } // otherwise search forward in the existing old children for an id set match @@ -254,18 +253,19 @@ var Idiomorph = (function () { insertionPoint, ctx, ); - continue; + return idSetMatch; } // if the current point is already a soft match morph if (isSoftMatch(insertionPoint, newChild)) { + const morphedNode = insertionPoint; insertionPoint = morphChild( insertionPoint, newChild, insertionPoint, ctx, ); - continue; + return morphedNode; } // search forward in the existing old children for a soft match for the current node @@ -282,7 +282,7 @@ var Idiomorph = (function () { insertionPoint, ctx, ); - continue; + return softMatch; } } @@ -296,12 +296,12 @@ var Idiomorph = (function () { ctx, ); morphNode(movedChild, newChild, ctx); - continue; + return movedChild; } // last resort, insert a new node from scratch - createNode(oldParent, newChild, insertionPoint, ctx); - } + return createNode(oldParent, newChild, insertionPoint, ctx); + }); // remove any remaining old nodes that didn't match up with new content while (insertionPoint && insertionPoint != endPoint) { @@ -309,6 +309,8 @@ var Idiomorph = (function () { insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } + + return morphedNodes; } /** @@ -336,11 +338,13 @@ var Idiomorph = (function () { oldParent.insertBefore(newEmptyChild, insertionPoint); morphNode(newEmptyChild, newChild, ctx); ctx.callbacks.afterNodeAdded(newEmptyChild); + return newEmptyChild; } else { // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent oldParent.insertBefore(newClonedChild, insertionPoint); ctx.callbacks.afterNodeAdded(newClonedChild); + return newClonedChild; } } @@ -659,7 +663,7 @@ var Idiomorph = (function () { morphAttributes(oldNode, newContent, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { // @ts-ignore newContent can be a node here because .firstChild will be null - morphChildren(oldNode, newContent, ctx); + morphChildren(ctx, oldNode, newContent); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); From 3a4831785d0958e1bf517eb34d128e10ea079076 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 02:35:42 -0600 Subject: [PATCH 22/50] tighten up init. --- src/idiomorph.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 14f45f6..62f9fc8 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -115,12 +115,12 @@ var Idiomorph = (function () { // AND NOW IT BEGINS... //============================================================================= - function noOp() {} + const noOp = () => {}; /** * Default configuration values, updatable by users now * @type {ConfigInternal} */ - let defaults = { + const defaults = { morphStyle: "outerHTML", callbacks: { beforeNodeAdded: noOp, @@ -133,12 +133,8 @@ var Idiomorph = (function () { }, head: { style: "merge", - shouldPreserve: function (elt) { - return elt.getAttribute("im-preserve") === "true"; - }, - shouldReAppend: function (elt) { - return elt.getAttribute("im-re-append") === "true"; - }, + shouldPreserve: (elt) => elt.getAttribute("im-preserve") === "true", + shouldReAppend: (elt) => elt.getAttribute("im-re-append") === "true", shouldRemove: noOp, afterHeadMorphed: noOp, }, From f433564de088b076fc593489c1f2ed89d66ff781 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 02:35:49 -0600 Subject: [PATCH 23/50] huge simplifying refactor. --- src/idiomorph.js | 61 +++++------------------------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 62f9fc8..325b37f 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -223,62 +223,13 @@ var Idiomorph = (function () { const morphedNodes = [...newParent.childNodes].map((newChild) => { // once we reach the end of the old parent content skip to the end and insert the rest if (insertionPoint != endPoint) { - // if the current node has an id set match then morph - if (isIdSetMatch(insertionPoint, newChild, ctx)) { - const morphedNode = insertionPoint; - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - return morphedNode; - } - - // otherwise search forward in the existing old children for an id set match - const idSetMatch = findIdSetMatch( - newChild, - insertionPoint, - endPoint, - ctx, - ); - if (idSetMatch) { - insertionPoint = morphChild( - idSetMatch, - newChild, - insertionPoint, - ctx, - ); - return idSetMatch; - } + const bestMatch = + findIdSetMatch(newChild, insertionPoint, endPoint, ctx) || + findSoftMatch(newChild, insertionPoint, endPoint, ctx); - // if the current point is already a soft match morph - if (isSoftMatch(insertionPoint, newChild)) { - const morphedNode = insertionPoint; - insertionPoint = morphChild( - insertionPoint, - newChild, - insertionPoint, - ctx, - ); - return morphedNode; - } - - // search forward in the existing old children for a soft match for the current node - const softMatch = findSoftMatch( - newChild, - insertionPoint, - endPoint, - ctx, - ); - if (softMatch) { - insertionPoint = morphChild( - softMatch, - newChild, - insertionPoint, - ctx, - ); - return softMatch; + if (bestMatch) { + insertionPoint = morphChild(bestMatch, newChild, insertionPoint, ctx); + return bestMatch; } } From e9821769937e04b288c8ef826e9122a27d8e8645 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 02:45:56 -0600 Subject: [PATCH 24/50] inline morphChild. --- src/idiomorph.js | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 325b37f..bd82d10 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -228,12 +228,17 @@ var Idiomorph = (function () { findSoftMatch(newChild, insertionPoint, endPoint, ctx); if (bestMatch) { - insertionPoint = morphChild(bestMatch, newChild, insertionPoint, ctx); + // if the node to morph is not at the insertion point then we need to move it here + if (bestMatch !== insertionPoint) { + moveBefore(oldParent, bestMatch, insertionPoint); + } + morphNode(bestMatch, newChild, ctx); + insertionPoint = bestMatch.nextSibling; return bestMatch; } } - // if the matching node is in the upcoming old content + // if the matching node is elsewhere in the original content if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { // move it and all its children here and morph const movedChild = moveBeforeById( @@ -246,7 +251,7 @@ var Idiomorph = (function () { return movedChild; } - // last resort, insert a new node from scratch + // last resort: insert the new node from scratch return createNode(oldParent, newChild, insertionPoint, ctx); }); @@ -260,23 +265,6 @@ var Idiomorph = (function () { return morphedNodes; } - /** - * @param {Node} oldNode the node to be morphed - * @param {Node} newChild the new content - * @param {Node|null} insertionPoint the current point in the DOM we are morphing content at - * @param {MorphContext} ctx the merge context - * @returns {Node|null} returns the new insertion point after the merged node - */ - function morphChild(oldNode, newChild, insertionPoint, ctx) { - // if the node to morph is not at the insertion point then we need to move it here by moving or removing nodes - if (oldNode !== insertionPoint) { - // @ts-ignore we know the Node has a valid parent - moveBefore(oldNode.parentElement, oldNode, insertionPoint); - } - morphNode(oldNode, newChild, ctx); - return oldNode.nextSibling; - } - function createNode(oldParent, newChild, insertionPoint, ctx) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { From e4eeb8a3d6abaadbdbb14ef13012682427982ad5 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 03:29:07 -0600 Subject: [PATCH 25/50] satisfy tsc. --- src/idiomorph.js | 68 +++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index bd82d10..71b1b3e 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -148,30 +148,35 @@ var Idiomorph = (function () { * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] - * @returns {undefined | Node[]} + * @returns {Promise | Node[]} */ function morph(oldNode, newContent, config = {}) { oldNode = normalizeElement(oldNode); const newNode = normalizeParent(newContent); const ctx = createMorphContext(oldNode, newNode, config); - return withHeadBlocking(ctx, oldNode, newNode, (ctx) => { - let morphedNodes; - if (ctx.morphStyle === "innerHTML") { - morphedNodes = morphChildren(ctx, oldNode, newNode); - } else { - // outerHTML - morphedNodes = morphChildren( - ctx, - normalizeParent(oldNode), - newNode, - oldNode, - oldNode.nextSibling, - ); - } - ctx.pantry.remove(); - return morphedNodes; - }); + return withHeadBlocking( + ctx, + oldNode, + newNode, + /** @param {MorphContext} ctx */ (ctx) => { + let morphedNodes; + if (ctx.morphStyle === "innerHTML") { + morphedNodes = morphChildren(ctx, oldNode, newNode); + } else { + // outerHTML + morphedNodes = morphChildren( + ctx, + normalizeParent(oldNode), + newNode, + oldNode, + oldNode.nextSibling, + ); + } + ctx.pantry.remove(); + return morphedNodes; + }, + ); } const morphChildren = (function () { @@ -197,16 +202,16 @@ var Idiomorph = (function () { * @param {MorphContext} ctx the merge context * @param {Element} oldParent the old content that we are merging the new content into * @param {Element} newParent the parent element of the new content - * @param {Element | null} insertionPoint the point in the DOM we start morphing at (defaults to first child) - * @param {Element | null} endPoint the point in the DOM we stop morphing at (defaults to after last child) + * @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child) + * @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child) * @returns {Node[]} */ function morphChildren( ctx, oldParent, newParent, - insertionPoint, - endPoint, + insertionPoint = null, + endPoint = null, ) { if ( oldParent instanceof HTMLTemplateElement && @@ -262,11 +267,19 @@ var Idiomorph = (function () { removeNode(tempNode, ctx); } - return morphedNodes; + return morphedNodes.filter((e) => e != null); } + /** + * + * @param {Element} oldParent + * @param {Node} newChild + * @param {Node|null} insertionPoint + * @param {MorphContext} ctx + * @returns {Node|null} + */ function createNode(oldParent, newChild, insertionPoint, ctx) { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null; if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm const newEmptyChild = document.createElement(newChild.tagName); @@ -803,7 +816,8 @@ var Idiomorph = (function () { * @param {MorphContext} ctx * @param {Element} oldNode * @param {Element} newNode - * @returns {undefined | Node[]} + * @param {function} callback + * @returns {Node[] | Promise} */ function withHeadBlocking(ctx, oldNode, newNode, callback) { if (ctx.head.block) { @@ -1123,7 +1137,7 @@ var Idiomorph = (function () { /** * - * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent + * @param {Element | Document} content * @returns {Element} */ function normalizeElement(content) { @@ -1136,7 +1150,7 @@ var Idiomorph = (function () { /** * - * @param {null | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent + * @param {null | string | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent * @returns {Element} */ function normalizeParent(newContent) { From 14692b40b8db4cdde3e4d89fac7a2b6a6f04ae86 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Wed, 15 Jan 2025 02:01:48 +1300 Subject: [PATCH 26/50] Fix beforeNodeRemoved hook case and improve test and inline documentation --- src/idiomorph.js | 9 ++++---- test/hooks.js | 10 +++++++++ test/retain-hidden-state.js | 45 +++++++++---------------------------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 71b1b3e..a5caf57 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -141,9 +141,7 @@ var Idiomorph = (function () { }; /** - * ============================================================================= - * Core Morphing Algorithm - morph, morphChildren, morphNode - * ============================================================================= + * Core idiomorph function for morphing one DOM tree to another * * @param {Element | Document} oldNode * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent @@ -271,6 +269,8 @@ var Idiomorph = (function () { } /** + * This performs the action of inserting a new node while handling situations where the node contains + * elements with persistent ids and possible state info we can still preserve by moving in and then morphing * * @param {Element} oldParent * @param {Node} newChild @@ -842,9 +842,8 @@ var Idiomorph = (function () { } /** - * ============================================================================= * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style - * ============================================================================= + * * @param {Element} oldHead * @param {Element} newHead * @param {MorphContext} ctx diff --git a/test/hooks.js b/test/hooks.js index 01ef8d7..33d5c4e 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -121,6 +121,16 @@ describe("lifecycle hooks", function () { initial.outerHTML.should.equal("
  • A
  • B
"); }); + it("returning false to beforeNodeRemoved prevents removing the node with different tag types", function () { + let initial = make("
ABC
"); + Idiomorph.morph(initial, "
B
", { + callbacks: { + beforeNodeRemoved: (node) => false, + }, + }); + initial.outerHTML.should.equal("
ABC
"); + }); + it("calls afterNodeRemoved after a node is removed from the DOM", function () { let calls = []; let initial = make("
  • A
  • B
"); diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index b15706e..ce410c6 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -714,46 +714,23 @@ describe("algorithm", function () { getWorkArea().innerHTML.should.equal(finalSrc); }); - it("check moveBefore function falls back to insertBefore if moveBefore fails", function () { - getWorkArea().innerHTML = ` -
- - -
- `; - document.getElementById("focus").focus(); - // break the moveBefore function so it fails back to insertBefore and focus is not preserved - document.getElementById("focus").parentNode.moveBefore = true; - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - }); - it("check moveBefore function falls back to insertBefore if moveBefore is missing", function () { - getWorkArea().innerHTML = ` + getWorkArea().append( + make(`
- - + +
- `; - document.getElementById("focus").focus(); - // remove the moveBefore function so it fails back to insertBefore and focus is not preserved - document.getElementById("focus").parentNode.moveBefore = undefined; + `), + ); + // disable moveBefore function to force it to use insertBefore + document.getElementById("first").parentNode.moveBefore = undefined; + document.getElementById("second").parentNode.moveBefore = undefined; let finalSrc = `
- - + +
`; Idiomorph.morph(getWorkArea(), finalSrc, { From d25716c6c11111d7bffa74436c61a1c55f50a9cf Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Wed, 15 Jan 2025 02:07:10 +1300 Subject: [PATCH 27/50] use removeNodesBetween to fix issue exposed by hook test. --- src/idiomorph.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index a5caf57..8e00e83 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -225,15 +225,15 @@ var Idiomorph = (function () { // run through all the new content const morphedNodes = [...newParent.childNodes].map((newChild) => { // once we reach the end of the old parent content skip to the end and insert the rest - if (insertionPoint != endPoint) { + if (insertionPoint && insertionPoint != endPoint) { const bestMatch = findIdSetMatch(newChild, insertionPoint, endPoint, ctx) || findSoftMatch(newChild, insertionPoint, endPoint, ctx); if (bestMatch) { - // if the node to morph is not at the insertion point then we need to move it here + // if the node to morph is not at the insertion point then delete up to it if (bestMatch !== insertionPoint) { - moveBefore(oldParent, bestMatch, insertionPoint); + removeNodesBetween(insertionPoint, bestMatch, ctx); } morphNode(bestMatch, newChild, ctx); insertionPoint = bestMatch.nextSibling; @@ -471,6 +471,24 @@ var Idiomorph = (function () { } } + /** + * + * @param {Node} startInclusive + * @param {Node} endExclusive + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function removeNodesBetween(startInclusive, endExclusive, ctx) { + /** @type {Node | null} */ let cursor = startInclusive; + // remove nodes until it the end point + while (cursor && cursor !== endExclusive) { + let tempNode = /** @type {Node} */ (cursor); + cursor = cursor.nextSibling; + removeNode(tempNode, ctx); + } + return cursor; + } + /** * Search for an element by id within the document and pantry, and move it using moveBefore. * From c7811f2c02657fed02b373332585861cbe1a0c26 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 10:54:09 -0600 Subject: [PATCH 28/50] split off preserve focus tests into their own file and move hook tests into hook file. --- test/hooks.js | 117 +++++++++++++ test/index.html | 1 + test/preserve-focus.js | 207 +++++++++++++++++++++++ test/retain-hidden-state.js | 318 +----------------------------------- 4 files changed, 326 insertions(+), 317 deletions(-) create mode 100644 test/preserve-focus.js diff --git a/test/hooks.js b/test/hooks.js index 33d5c4e..dbc289d 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -216,4 +216,121 @@ describe("lifecycle hooks", function () { }); initial.outerHTML.should.equal(``); }); + + it("hooks work as expected", function () { + let beginSrc = ` +
+ + +
+ `.trim(); + getWorkArea().append(make(beginSrc)); + + let finalSrc = ` +
+ + +
+ `.trim(); + + let wrongHookCalls = []; + let wrongHookHandler = (name) => { + return (node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + wrongHookCalls.push([name, node.outerHTML]); + }; + }; + + let calls = []; + + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + callbacks: { + beforeNodeAdded: wrongHookHandler("beforeNodeAdded"), + afterNodeAdded: wrongHookHandler("afterNodeAdded"), + beforeNodeRemoved: wrongHookHandler("beforeNodeRemoved"), + afterNodeRemoved: wrongHookHandler("afterNodeRemoved"), + beforeNodeMorphed: (oldNode, newNode) => { + if (oldNode.nodeType !== Node.ELEMENT_NODE) return; + calls.push(["before", oldNode.outerHTML, newNode.outerHTML]); + }, + afterNodeMorphed: (oldNode, newNode) => { + if (oldNode.nodeType !== Node.ELEMENT_NODE) return; + calls.push(["after", oldNode.outerHTML, newNode.outerHTML]); + }, + }, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + + wrongHookCalls.should.eql([]); + calls.should.eql([ + ["before", beginSrc, finalSrc], + [ + "before", + ``, + ``, + ], + [ + "after", + ``, + ``, + ], + [ + "before", + ``, + ``, + ], + [ + "after", + ``, + ``, + ], + [ + "after", + '
\n \n \n
', + '
\n \n \n
', + ], + ]); + }); + + it("beforeNodeMorphed hook also applies to nodes restored from the pantry", function () { + getWorkArea().append( + make(` +
+

First paragraph

+

Second paragraph

+
+ `), + ); + document.getElementById("first").innerHTML = "First paragraph EDITED"; + document.getElementById("second").innerHTML = "Second paragraph EDITED"; + + let finalSrc = ` +
+

Second paragraph

+

First paragraph

+
+ `; + + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + callbacks: { + // basic implementation of a preserve-me attr + beforeNodePantried(node) { + if (node.parentNode?.dataset?.preserveMe) return false; + }, + beforeNodeMorphed(oldNode, newContent) { + if (oldNode.dataset?.preserveMe) return false; + }, + }, + }); + + getWorkArea().innerHTML.should.equal(` +
+

Second paragraph EDITED

+

First paragraph EDITED

+
+ `); + }); }); diff --git a/test/index.html b/test/index.html index 29f5c08..fe56326 100644 --- a/test/index.html +++ b/test/index.html @@ -45,6 +45,7 @@

Mocha Test Suite

+
diff --git a/test/preserve-focus.js b/test/preserve-focus.js new file mode 100644 index 0000000..ba9e304 --- /dev/null +++ b/test/preserve-focus.js @@ -0,0 +1,207 @@ +describe("Preserves focus where possible", function () { + setup(); + + it("preserves focus state and outerHTML morphStyle", function () { + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + document.getElementById("first").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("first").outerHTML, + ); + } else { + // TODO + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + } + }); + + it("preserves focus state when previous element is replaced", function () { + getWorkArea().innerHTML = ` +
+ + +
+ `; + document.getElementById("focus").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + document.activeElement.outerHTML.should.equal( + document.getElementById("focus").outerHTML, + ); + }); + + it("preserves focus state when elements are moved to different levels of the DOM", function () { + getWorkArea().append( + make(` +
+ +
+ +
+
+ `), + ); + document.getElementById("second").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("second").outerHTML, + ); + } else { + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + console.log( + "preserves focus state when elements are moved to different levels of the DOM test needs moveBefore enabled to work properly", + ); + } + }); + + it("preserves focus state when focused element is moved between anonymous containers", function () { + getWorkArea().innerHTML = ` +
+ +
+
+ +
+ `; + document.getElementById("second").focus(); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("second").outerHTML, + ); + } else { + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + console.log("needs moveBefore enabled to work properly"); + } + }); + + it("preserves focus state when elements are moved between IDed containers", function () { + getWorkArea().append( + make(` +
+
+ +
+ +
+ `), + ); + document.getElementById("first").focus(); + + let finalSrc = ` +
+
+ +
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("first").outerHTML, + ); + } else { + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + console.log( + "preserves focus state when elements are moved between IDed containers test needs moveBefore enabled to work properly", + ); + } + }); + + it("preserves focus state when parents are reordered", function () { + getWorkArea().append( + make(` +
+
+ +
+
+ +
+
+ `), + ); + document.getElementById("focus").focus(); + + let finalSrc = ` +
+
+ +
+
+ +
+
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + if (document.body.moveBefore) { + document.activeElement.outerHTML.should.equal( + document.getElementById("focus").outerHTML, + ); + } else { + // TODO + document.activeElement.outerHTML.should.equal(document.body.outerHTML); + } + }); +}); diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index ce410c6..c9b1bd6 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -1,4 +1,4 @@ -describe("algorithm", function () { +describe("Hidden state preservation tests", function () { setup(); it("preserves all non-attribute element state", function () { @@ -161,322 +161,6 @@ describe("algorithm", function () { states.should.eql([true, true]); }); - it("preserves focus state and outerHTML morphStyle", function () { - const div = make(` -
- - -
- `); - getWorkArea().append(div); - document.getElementById("first").focus(); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); - - getWorkArea().innerHTML.should.equal(finalSrc); - document.activeElement.outerHTML.should.equal( - document.getElementById("first").outerHTML, - ); - }); - - it("preserves focus state when previous element is replaced", function () { - getWorkArea().innerHTML = ` -
- - -
- `; - document.getElementById("focus").focus(); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("focus").outerHTML, - ); - } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log("needs moveBefore enabled to work properly"); - } - }); - - it("preserves focus state when elements are moved to different levels of the DOM", function () { - getWorkArea().append( - make(` -
- -
- -
-
- `), - ); - document.getElementById("second").focus(); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("second").outerHTML, - ); - } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log( - "preserves focus state when elements are moved to different levels of the DOM test needs moveBefore enabled to work properly", - ); - } - }); - - it("preserves focus state when focused element is moved between anonymous containers", function () { - getWorkArea().innerHTML = ` -
- -
-
- -
- `; - document.getElementById("second").focus(); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("second").outerHTML, - ); - } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log("needs moveBefore enabled to work properly"); - } - }); - - it("preserves focus state when elements are moved between IDed containers", function () { - getWorkArea().append( - make(` -
-
- -
- -
- `), - ); - document.getElementById("first").focus(); - - let finalSrc = ` -
-
- -
- -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("first").outerHTML, - ); - } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log( - "preserves focus state when elements are moved between IDed containers test needs moveBefore enabled to work properly", - ); - } - }); - - it("preserves focus state when parents are reordered", function () { - getWorkArea().append( - make(` -
-
- -
-
- -
-
- `), - ); - document.getElementById("focus").focus(); - - let finalSrc = ` -
-
- -
-
- -
-
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - document.activeElement.outerHTML.should.equal( - document.getElementById("focus").outerHTML, - ); - getWorkArea().innerHTML.should.equal(finalSrc); - }); - - it("hooks work as expected", function () { - let beginSrc = ` -
- - -
- `.trim(); - getWorkArea().append(make(beginSrc)); - - let finalSrc = ` -
- - -
- `.trim(); - - let wrongHookCalls = []; - let wrongHookHandler = (name) => { - return (node) => { - if (node.nodeType !== Node.ELEMENT_NODE) return; - wrongHookCalls.push([name, node.outerHTML]); - }; - }; - - let calls = []; - - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - callbacks: { - beforeNodeAdded: wrongHookHandler("beforeNodeAdded"), - afterNodeAdded: wrongHookHandler("afterNodeAdded"), - beforeNodeRemoved: wrongHookHandler("beforeNodeRemoved"), - afterNodeRemoved: wrongHookHandler("afterNodeRemoved"), - beforeNodeMorphed: (oldNode, newNode) => { - if (oldNode.nodeType !== Node.ELEMENT_NODE) return; - calls.push(["before", oldNode.outerHTML, newNode.outerHTML]); - }, - afterNodeMorphed: (oldNode, newNode) => { - if (oldNode.nodeType !== Node.ELEMENT_NODE) return; - calls.push(["after", oldNode.outerHTML, newNode.outerHTML]); - }, - }, - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - - wrongHookCalls.should.eql([]); - calls.should.eql([ - ["before", beginSrc, finalSrc], - [ - "before", - ``, - ``, - ], - [ - "after", - ``, - ``, - ], - [ - "before", - ``, - ``, - ], - [ - "after", - ``, - ``, - ], - [ - "after", - '
\n \n \n
', - '
\n \n \n
', - ], - ]); - }); - - it("beforeNodeMorphed hook also applies to nodes restored from the pantry", function () { - getWorkArea().append( - make(` -
-

First paragraph

-

Second paragraph

-
- `), - ); - document.getElementById("first").innerHTML = "First paragraph EDITED"; - document.getElementById("second").innerHTML = "Second paragraph EDITED"; - - let finalSrc = ` -
-

Second paragraph

-

First paragraph

-
- `; - - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - callbacks: { - // basic implementation of a preserve-me attr - beforeNodePantried(node) { - if (node.parentNode?.dataset?.preserveMe) return false; - }, - beforeNodeMorphed(oldNode, newContent) { - if (oldNode.dataset?.preserveMe) return false; - }, - }, - }); - - getWorkArea().innerHTML.should.equal(` -
-

Second paragraph EDITED

-

First paragraph EDITED

-
- `); - }); - it("duplicate ids on elements aborts matching to avoid invalid morph state", function () { // we try to reuse existing ids where possible and has to exclude matching on duplicate ids // to avoid losing content From 8e7e09f82c54bd7b6129afe8402c9bd29f1e1275 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 11:01:11 -0600 Subject: [PATCH 29/50] back to 100% coverage. --- test/core.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/core.js b/test/core.js index d13459c..2847d0d 100644 --- a/test/core.js +++ b/test/core.js @@ -1,13 +1,27 @@ describe("Core morphing tests", function () { setup(); + it("morphs outerHTML by default", function () { + let initial = make(""); + let finalSrc = ""; + Idiomorph.morph(initial, finalSrc, { morphStyle: "outerHTML" }); + initial.outerHTML.should.equal(""); + }); + + it("morphs outerHTML if morphStyle is missing", function () { + let initial = make(""); + let finalSrc = ""; + Idiomorph.morph(initial, finalSrc, { morphStyle: null }); + initial.outerHTML.should.equal(""); + }); + it("morphs outerHTML as content properly when argument is null", function () { let initial = make(""); Idiomorph.morph(initial, null, { morphStyle: "outerHTML" }); initial.isConnected.should.equal(false); }); - it("morphs outerHTML as content properly when argument is single node string", function () { + it("morphs outerHTML as content properly when argument is single node", function () { let initial = make(""); let finalSrc = ""; let final = make(finalSrc); From eeb326365255bcb7aa6152852d3c71c036864f26 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 11:25:15 -0600 Subject: [PATCH 30/50] tweak inner loop docs and type checking. --- src/idiomorph.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 8e00e83..d156c75 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -183,16 +183,17 @@ var Idiomorph = (function () { * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but * by using id sets, we are able to better match up with content deeper in the DOM. * - * Basic algorithm is, for each node in the new content: - * - * - if we have not reached the end of the old parent: - * - if the new content has an id set match with the current insertion point, morph - * - search for an id set match - * - if id set match found, morph - * - if the new content is a soft match with the current insertion point, morph - * - otherwise search for a "soft" match - * - if a soft match is found, morph - * - otherwise, prepend the new node before the current insertion point + * Basic algorithm: + * - for each node in the new content: + * - if there could be a match among self and upcoming siblings + * - search self and siblings for an id set match, falling back to a soft match + * - if found + * - remove any nodes inbetween (pantrying any persistent nodes) + * - morph it and move on + * - if no match and node is persistent + * - move it and all its children here (looking in pantry too) + * - morph it and move on + * - create a new node from scratch as a last result * * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved * with the current node. See findIdSetMatch and findSoftMatch for details. @@ -211,6 +212,7 @@ var Idiomorph = (function () { insertionPoint = null, endPoint = null, ) { + // normalize if ( oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement @@ -242,11 +244,11 @@ var Idiomorph = (function () { } // if the matching node is elsewhere in the original content - if (ctx.persistentIds.has(/** @type {Element} */ (newChild).id)) { + if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) { // move it and all its children here and morph const movedChild = moveBeforeById( oldParent, - /** @type {Element} */ (newChild).id, + newChild.id, insertionPoint, ctx, ); From 77ebe639702d84ff777e8f3ff5aa0bc2dd3f429b Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 11:33:15 -0600 Subject: [PATCH 31/50] no longer need to be careful about losing state now that we're moving, rather than deleting. --- src/idiomorph.js | 64 ++++--------------------------------- test/retain-hidden-state.js | 34 -------------------- 2 files changed, 6 insertions(+), 92 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index d156c75..d74b527 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -195,9 +195,6 @@ var Idiomorph = (function () { * - morph it and move on * - create a new node from scratch as a last result * - * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved - * with the current node. See findIdSetMatch and findSoftMatch for details. - * * @param {MorphContext} ctx the merge context * @param {Element} oldParent the old content that we are merging the new content into * @param {Element} newParent the parent element of the new content @@ -322,14 +319,11 @@ var Idiomorph = (function () { /** * - * @param {Node | null} oldNode - * @param {Node | null} newNode + * @param {Node} oldNode + * @param {Node} newNode * @returns {boolean} */ function isSoftMatch(oldNode, newNode) { - if (oldNode == null || newNode == null) { - return false; - } // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing if ( @@ -349,9 +343,7 @@ var Idiomorph = (function () { /** * ============================================================================= * Scans forward from the insertionPoint in the old parent looking for a potential id match - * for the newChild. We stop if we find a potential id match for the new child OR - * if the number of potential id matches we are discarding is greater than the - * potential id matches for the new child + * for the newChild. * ============================================================================= * @param {Node} newChild * @param {Node | null} insertionPoint @@ -360,37 +352,16 @@ var Idiomorph = (function () { * @returns {Node | null} */ function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { - // max id matches we are willing to discard in our search - let newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); - - /** - * @type {Node | null} - */ + const newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); // only search forward if there is a possibility of an id match if (newChildPotentialIdCount > 0) { let potentialMatch = insertionPoint; - // if there is a possibility of an id match, scan forward - // keep track of the potential id match count we are discarding (the - // newChildPotentialIdCount must be greater than this to make it likely - // worth it) - let otherMatchCount = 0; - while (potentialMatch != null && potentialMatch != endPoint) { + while (potentialMatch && potentialMatch != endPoint) { // If we have an id match, return the current potential match if (isIdSetMatch(potentialMatch, newChild, ctx)) { return potentialMatch; } - - // computer the other potential matches of this new content - otherMatchCount += getPersistentIdNodeCount(ctx, potentialMatch); - - if (otherMatchCount > newChildPotentialIdCount) { - // if we have more potential id matches in _other_ content, we - // do not have a good candidate for an id match, so return null - return null; - } - - // advanced to the next old content child potentialMatch = potentialMatch.nextSibling; } } @@ -411,42 +382,19 @@ var Idiomorph = (function () { * @returns {null | Node} */ function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { - /** - * @type {Node | null} - */ let potentialSoftMatch = insertionPoint; - /** - * @type {Node | null} - */ let nextSibling = newChild.nextSibling; - let siblingSoftMatchCount = 0; - while (potentialSoftMatch != null && potentialSoftMatch != endPoint) { + while (potentialSoftMatch && potentialSoftMatch != endPoint) { if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { // the current potential soft match has a potential id set match with the remaining new // content so bail out of looking return null; } - // if we have a soft match with the current node, return it if (isSoftMatch(potentialSoftMatch, newChild)) { return potentialSoftMatch; } - - if (isSoftMatch(potentialSoftMatch, nextSibling)) { - // the next new node has a soft match with this node, so - // increment the count of future soft matches - siblingSoftMatchCount++; - // ok to cast: if it was null it couldn't be a soft match - nextSibling = /** @type {Node} */ (nextSibling).nextSibling; - - // If there are two future soft matches, bail to allow the siblings to soft match - // so that we don't consume future soft matches for the sake of the current node - if (siblingSoftMatchCount >= 2) { - return null; - } - } - // advanced to the next old content child potentialSoftMatch = potentialSoftMatch.nextSibling; } diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index c9b1bd6..7041aa2 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -338,40 +338,6 @@ describe("Hidden state preservation tests", function () { states.should.eql([true]); }); - it("findIdSetMatch rejects morphing node that would lose more IDs", function () { - const div = make(` -
- - - -
- `); - getWorkArea().append(div); - document.getElementById("third").focus(); - - let finalSrc = ` -
- - - -
- `; - Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); - - getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - // moveBefore would prevent the node being discarded and losing state so we can't detect easily if findIdSetMatch rejected the morphing - document.activeElement.outerHTML.should.equal( - document.getElementById("third").outerHTML, - ); - } else { - // but testing with no moveBefore we can test it - // third paragrah should have been discarded because it was moved in front of two other paragraphs with ID's - // it should detect that removing the first two nodes with ID's to preserve just one ID is not worth it - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - } - }); - it("check moveBefore function fails back to insertBefore if moveBefore fails", function () { getWorkArea().append( make(` From 82392ba0591f39428a05351b11e083f00b430259 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 12:17:47 -0600 Subject: [PATCH 32/50] test moveBefore -> insertBefore fallback. --- test/retain-hidden-state.js | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index 7041aa2..ee0c5b5 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -338,7 +338,7 @@ describe("Hidden state preservation tests", function () { states.should.eql([true]); }); - it("check moveBefore function fails back to insertBefore if moveBefore fails", function () { + it("moveBefore function fails back to insertBefore if moveBefore fails", function () { getWorkArea().append( make(`
@@ -349,7 +349,6 @@ describe("Hidden state preservation tests", function () { ); // replace moveBefore function with a boolean which will fail the try catch document.getElementById("first").parentNode.moveBefore = true; - document.getElementById("second").parentNode.moveBefore = true; let finalSrc = `
@@ -364,7 +363,7 @@ describe("Hidden state preservation tests", function () { getWorkArea().innerHTML.should.equal(finalSrc); }); - it("check moveBefore function falls back to insertBefore if moveBefore is missing", function () { + it("moveBefore function falls back to insertBefore if moveBefore is missing", function () { getWorkArea().append( make(`
@@ -375,7 +374,6 @@ describe("Hidden state preservation tests", function () { ); // disable moveBefore function to force it to use insertBefore document.getElementById("first").parentNode.moveBefore = undefined; - document.getElementById("second").parentNode.moveBefore = undefined; let finalSrc = `
@@ -388,6 +386,34 @@ describe("Hidden state preservation tests", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - document.activeElement.outerHTML.should.equal(document.body.outerHTML); + }); + + it("moveBefore is used if it exists", function () { + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + + let called = false; + div.moveBefore = function(element, after) { + called = true; + return div.insertBefore(element, after); + } + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + called.should.be.true; }); }); From d6101ce577fd962f237295d649c674fb0deeeed5 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 12 Jan 2025 19:36:34 -0600 Subject: [PATCH 33/50] add restoreFocus option for manually saving and restoring any lost focus and/or selection. --- README.md | 1 + src/idiomorph.js | 88 ++++++++--- test/index.html | 1 + test/lib/utilities.js | 33 +++++ test/preserve-focus.js | 155 ++++++++++++-------- test/restore-focus.js | 281 ++++++++++++++++++++++++++++++++++++ test/retain-hidden-state.js | 4 +- 7 files changed, 475 insertions(+), 88 deletions(-) create mode 100644 test/restore-focus.js diff --git a/README.md b/README.md index 2274259..b2aff3c 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Idiomorph supports the following options: | `morphStyle` | The style of morphing to use, either `innerHTML` or `outerHTML` | `Idiomorph.morph(..., {morphStyle:'innerHTML'})` | | `ignoreActive` | If set to `true`, idiomorph will skip the active element | `Idiomorph.morph(..., {ignoreActive:true})` | | `ignoreActiveValue` | If set to `true`, idiomorph will not update the active element's value | `Idiomorph.morph(..., {ignoreActiveValue:true})` | +| `restoreFocus` | If set to `true`, idiomorph will attempt to restore any lost focus and selection state after the morph. | `Idiomorph.morph(..., {restoreFocus:true})` | | `head` | Allows you to control how the `head` tag is merged. See the [head](#the-head-tag) section for more details | `Idiomorph.morph(..., {head:{style:merge}})` | | `callbacks` | Allows you to insert callbacks when events occur in the morph life cycle, see the callback table below | `Idiomorph.morph(..., {callbacks:{beforeNodeAdded:function(node){...}})` | diff --git a/src/idiomorph.js b/src/idiomorph.js index d74b527..3d17d79 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -28,6 +28,7 @@ * @property {'outerHTML' | 'innerHTML'} [morphStyle] * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] + * @property {boolean} [restoreFocus] * @property {ConfigCallbacks} [callbacks] * @property {ConfigHead} [head] */ @@ -68,6 +69,7 @@ * @property {'outerHTML' | 'innerHTML'} morphStyle * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] + * @property {boolean} [restoreFocus] * @property {ConfigCallbacksInternal} callbacks * @property {ConfigHeadInternal} head */ @@ -104,6 +106,7 @@ var Idiomorph = (function () { * @property {ConfigInternal['morphStyle']} morphStyle * @property {ConfigInternal['ignoreActive']} ignoreActive * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue + * @property {ConfigInternal['restoreFocus']} restoreFocus * @property {Map>} idMap * @property {Set} persistentIds * @property {ConfigInternal['callbacks']} callbacks @@ -138,6 +141,7 @@ var Idiomorph = (function () { shouldRemove: noOp, afterHeadMorphed: noOp, }, + restoreFocus: false, }; /** @@ -153,28 +157,67 @@ var Idiomorph = (function () { const newNode = normalizeParent(newContent); const ctx = createMorphContext(oldNode, newNode, config); - return withHeadBlocking( - ctx, - oldNode, - newNode, - /** @param {MorphContext} ctx */ (ctx) => { - let morphedNodes; - if (ctx.morphStyle === "innerHTML") { - morphedNodes = morphChildren(ctx, oldNode, newNode); - } else { - // outerHTML - morphedNodes = morphChildren( - ctx, - normalizeParent(oldNode), - newNode, - oldNode, - oldNode.nextSibling, - ); - } - ctx.pantry.remove(); - return morphedNodes; - }, - ); + return saveAndRestoreFocus(ctx, () => { + return withHeadBlocking( + ctx, + oldNode, + newNode, + /** @param {MorphContext} ctx */ (ctx) => { + let morphedNodes; + if (ctx.morphStyle === "innerHTML") { + morphedNodes = morphChildren(ctx, oldNode, newNode); + } else { + // outerHTML + morphedNodes = morphChildren( + ctx, + normalizeParent(oldNode), + newNode, + oldNode, + oldNode.nextSibling, + ); + } + ctx.pantry.remove(); + return morphedNodes; + }, + ); + }); + } + + /** + * @param {MorphContext} ctx + * @param {Function} fn + * @returns {Promise | Node[]} + */ + function saveAndRestoreFocus(ctx, fn) { + if (!ctx.config.restoreFocus) return fn(); + + let activeElement = document.activeElement; + + // don't bother if the active element is not an input or textarea + if ( + !( + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement + ) + ) { + return fn(); + } + + const { id: activeElementId, selectionStart, selectionEnd } = activeElement; + + const results = fn(); + + if (activeElementId && activeElementId !== document.activeElement?.id) { + activeElement = ctx.target.querySelector(`#${activeElementId}`); + // @ts-ignore we can assume this is focusable + activeElement?.focus(); + } + if (activeElement && selectionStart && selectionEnd) { + // @ts-ignore we know this is an input element + activeElement.setSelectionRange(selectionStart, selectionEnd); + } + + return results; } const morphChildren = (function () { @@ -943,6 +986,7 @@ var Idiomorph = (function () { morphStyle: morphStyle, ignoreActive: mergedConfig.ignoreActive, ignoreActiveValue: mergedConfig.ignoreActiveValue, + restoreFocus: mergedConfig.restoreFocus, idMap: idMap, persistentIds: persistentIds, pantry: createPantry(), diff --git a/test/index.html b/test/index.html index fe56326..340c86f 100644 --- a/test/index.html +++ b/test/index.html @@ -46,6 +46,7 @@

Mocha Test Suite

+
diff --git a/test/lib/utilities.js b/test/lib/utilities.js index ebd674f..908a1be 100644 --- a/test/lib/utilities.js +++ b/test/lib/utilities.js @@ -9,6 +9,10 @@ function setup() { }); } +function hasMoveBefore() { + return !!document.body.moveBefore; +} + function make(htmlStr) { let range = document.createRange(); let fragment = range.createContextualFragment(htmlStr); @@ -57,3 +61,32 @@ function print(elt) { getWorkArea().appendChild(text); return elt; } + +function setFocusAndSelection(elementId, selectedText) { + const element = document.getElementById(elementId); + const value = element.value + const index = value.indexOf(selectedText); + if(index === -1) throw `"${value}" does not contain "${selectedText}"`; + element.focus(); + element.setSelectionRange(index, index + selectedText.length); +} + +function setFocus(elementId) { + document.getElementById(elementId).focus(); +} + +function assertFocusAndSelection(elementId, selectedText) { + assertFocus(elementId); + const activeElement = document.activeElement; + activeElement.id.should.eql(elementId); + activeElement.value.substring(activeElement.selectionStart, activeElement.selectionEnd).should.eql(selectedText); +} + +function assertFocus(elementId) { + document.activeElement.id.should.eql(elementId); +} + +function assertNoFocus() { + document.activeElement.should.equal(document.body); +} + diff --git a/test/preserve-focus.js b/test/preserve-focus.js index ba9e304..c81391f 100644 --- a/test/preserve-focus.js +++ b/test/preserve-focus.js @@ -4,29 +4,29 @@ describe("Preserves focus where possible", function () { it("preserves focus state and outerHTML morphStyle", function () { const div = make(`
- - + +
`); getWorkArea().append(div); - document.getElementById("first").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
- - + +
`; Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("first").outerHTML, - ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); } else { - // TODO - document.activeElement.outerHTML.should.equal(document.body.outerHTML); + assertNoFocus(); } }); @@ -34,15 +34,15 @@ describe("Preserves focus where possible", function () { getWorkArea().innerHTML = `
- +
`; - document.getElementById("focus").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
- +
`; Idiomorph.morph(getWorkArea(), finalSrc, { @@ -50,28 +50,26 @@ describe("Preserves focus where possible", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - document.activeElement.outerHTML.should.equal( - document.getElementById("focus").outerHTML, - ); + assertFocusAndSelection("focused", "b"); }); it("preserves focus state when elements are moved to different levels of the DOM", function () { getWorkArea().append( make(`
- +
- +
`), ); - document.getElementById("second").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
- - + +
`; Idiomorph.morph(getWorkArea(), finalSrc, { @@ -79,33 +77,31 @@ describe("Preserves focus where possible", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("second").outerHTML, - ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log( - "preserves focus state when elements are moved to different levels of the DOM test needs moveBefore enabled to work properly", - ); + assertNoFocus(); } }); it("preserves focus state when focused element is moved between anonymous containers", function () { getWorkArea().innerHTML = `
- +
- +
`; - document.getElementById("second").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
- - + +
`; Idiomorph.morph(getWorkArea(), finalSrc, { @@ -113,13 +109,13 @@ describe("Preserves focus where possible", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("second").outerHTML, - ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log("needs moveBefore enabled to work properly"); + assertNoFocus(); } }); @@ -128,23 +124,23 @@ describe("Preserves focus where possible", function () { make(`
- +
`), ); - document.getElementById("first").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
- +
`; @@ -153,24 +149,22 @@ describe("Preserves focus where possible", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("first").outerHTML, - ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); } else { - document.activeElement.outerHTML.should.equal(document.body.outerHTML); - console.log( - "preserves focus state when elements are moved between IDed containers test needs moveBefore enabled to work properly", - ); + assertNoFocus(); } }); - it("preserves focus state when parents are reordered", function () { + it("preserves focus state when focus parent is moved down", function () { getWorkArea().append( make(`
- +
@@ -178,7 +172,7 @@ describe("Preserves focus where possible", function () {
`), ); - document.getElementById("focus").focus(); + setFocusAndSelection("focused", "b"); let finalSrc = `
@@ -186,7 +180,7 @@ describe("Preserves focus where possible", function () {
- +
`; @@ -195,13 +189,46 @@ describe("Preserves focus where possible", function () { }); getWorkArea().innerHTML.should.equal(finalSrc); - if (document.body.moveBefore) { - document.activeElement.outerHTML.should.equal( - document.getElementById("focus").outerHTML, - ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); } else { - // TODO - document.activeElement.outerHTML.should.equal(document.body.outerHTML); + assertNoFocus(); } }); + + it("preserves focus state when focus parent is moved up", function () { + getWorkArea().append( + make(` +
+
+ +
+
+ +
+
+ `), + ); + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+
+ +
+
+ +
+
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); }); diff --git a/test/restore-focus.js b/test/restore-focus.js new file mode 100644 index 0000000..607e005 --- /dev/null +++ b/test/restore-focus.js @@ -0,0 +1,281 @@ +describe("Option to forcibly restore focus after morph", function () { + setup(); + + it("restores focus and selection state with and outerHTML morphStyle", function () { + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(div, finalSrc, { + morphStyle: "outerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state when elements are moved to different levels of the DOM", function () { + getWorkArea().innerHTML = ` +
+ +
+ +
+
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state when elements are moved between different containers", function () { + getWorkArea().innerHTML = ` +
+
+ +
+ +
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+
+ +
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state when parents are reorderd", function () { + getWorkArea().innerHTML = ` +
+
+ +
+ +
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ +
+ +
+
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state with restoreFocus option and outerHTML morphStyle", function () { + const div = make(` +
+ + +
+ `); + getWorkArea().append(div); + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(div, finalSrc, { + morphStyle: "outerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state with restoreFocus option when elements are moved to different levels of the DOM", function () { + getWorkArea().innerHTML = ` +
+ +
+ +
+
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state with restoreFocus option when elements are moved between different containers", function () { + getWorkArea().innerHTML = ` +
+
+ +
+ +
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+
+ +
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state with restoreFocus option when parents are reordered", function () { + getWorkArea().innerHTML = ` +
+
+ +
+ +
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ +
+ +
+
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("restores focus and selection state with a textarea", function () { + getWorkArea().innerHTML = ` +
+ + +
+ `; + setFocusAndSelection("focused", "b"); + + let finalSrc = ` +
+ + +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusAndSelection("focused", "b"); + }); + + it("does nothing if a non input/textarea el is focused", function () { + getWorkArea().innerHTML = ` +
+

+

+
+ `; + setFocus("focused"); + + let finalSrc = ` +
+

+

+
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { + morphStyle: "innerHTML", + restoreFocus: true, + }); + + getWorkArea().innerHTML.should.equal(finalSrc); + assertNoFocus(); + }); +}); diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index ee0c5b5..560c08a 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -398,10 +398,10 @@ describe("Hidden state preservation tests", function () { getWorkArea().append(div); let called = false; - div.moveBefore = function(element, after) { + div.moveBefore = function (element, after) { called = true; return div.insertBefore(element, after); - } + }; let finalSrc = `
From 017f71412cf905de9509fb9b59e4a114d8a028e0 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 17:31:36 -0600 Subject: [PATCH 34/50] simplify softMatch more and tweak docs. --- src/idiomorph.js | 57 +++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 3d17d79..8539edc 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -384,10 +384,8 @@ var Idiomorph = (function () { } /** - * ============================================================================= - * Scans forward from the insertionPoint in the old parent looking for a potential id match - * for the newChild. - * ============================================================================= + * Scans forward from the insertionPoint in the old parent looking for a potential id match + * for the newChild. * @param {Node} newChild * @param {Node | null} insertionPoint * @param {Node | null} endPoint @@ -412,12 +410,8 @@ var Idiomorph = (function () { } /** - * ============================================================================= - * Scans forward from the insertionPoint in the old parent looking for a potential soft match - * for the newChild. We stop if we find a potential soft match for the new child OR - * if we find a potential id match in the old parents children OR if we find two - * potential soft matches for the next two pieces of new content - * ============================================================================= + * Scans forward from the insertionPoint in the old parent looking for a potential soft match + * for the newChild. * @param {Node} newChild * @param {Node | null} insertionPoint * @param {Node | null} endPoint @@ -429,18 +423,15 @@ var Idiomorph = (function () { let nextSibling = newChild.nextSibling; while (potentialSoftMatch && potentialSoftMatch != endPoint) { - if (hasPersistentIdNodes(ctx, potentialSoftMatch)) { - // the current potential soft match has a potential id set match with the remaining new - // content so bail out of looking - return null; - } - - if (isSoftMatch(potentialSoftMatch, newChild)) { - return potentialSoftMatch; + // the current potential soft match has a id set match with the remaining new + // content so leave this one for the future + if (!hasPersistentIdNodes(ctx, potentialSoftMatch)) { + if (isSoftMatch(potentialSoftMatch, newChild)) { + return potentialSoftMatch; + } } potentialSoftMatch = potentialSoftMatch.nextSibling; } - return null; } @@ -505,7 +496,7 @@ var Idiomorph = (function () { /** * Moves an element before another element within the same parent. - * Uses the proposed `moveBefore` API if available, otherwise falls back to `insertBefore`. + * Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`. * This is essentialy a forward-compat wrapper. * * @param {Element} parentNode - The parent node containing the after element. @@ -630,7 +621,7 @@ var Idiomorph = (function () { } /** - * syncs a given node with another node, copying over all attributes and + * syncs the oldNode to the newNode, copying over all attributes and * inner element state from the newNode to the oldNode * * @param {Node} oldNode the node to copy attributes & state to @@ -748,8 +739,8 @@ var Idiomorph = (function () { } /** - * @param {Element} oldElement element to sync the value to - * @param {Element} newElement element to sync the value from + * @param {Element} oldElement element to write the value to + * @param {Element} newElement element to read the value from * @param {string} attributeName the attribute name * @param {MorphContext} ctx the merge context */ @@ -861,17 +852,11 @@ var Idiomorph = (function () { * @returns {Promise[]} */ function handleHeadElement(oldHead, newHead, ctx) { - /** @type {Node[]} */ let added = []; - /** @type {Element[]} */ let removed = []; - /** @type {Element[]} */ let preserved = []; - /** @type {Element[]} */ let nodesToAppend = []; - let headMergeStyle = ctx.head.style; - // put all new head elements into a Map, by their outerHTML let srcToNewHeadNodes = new Map(); for (const newHeadChild of newHead.children) { @@ -895,7 +880,7 @@ var Idiomorph = (function () { preserved.push(currentHeadElt); } } else { - if (headMergeStyle === "append") { + if (ctx.head.style === "append") { // we are appending and this existing element is not new content // so if and only if it is marked for re-append do we do anything if (isReAppended) { @@ -1002,9 +987,6 @@ var Idiomorph = (function () { * @returns {ConfigInternal} */ function mergeDefaults(config) { - /** - * @type {ConfigInternal} - */ let finalConfig = Object.assign({}, defaults); // copy top level stuff into final config @@ -1023,6 +1005,9 @@ var Idiomorph = (function () { return finalConfig; } + /** + * @returns {HTMLDivElement} + */ function createPantry() { const pantry = document.createElement("div"); pantry.hidden = true; @@ -1116,10 +1101,8 @@ var Idiomorph = (function () { persistentIds.add(id); } } - /** - * - * @type {Map>} - */ + + /** @type {Map>} */ let idMap = new Map(); populateIdMapForNode( oldContent.parentElement, From a327af93cfa01374bbc69179f9dfa292e5b9f649 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Tue, 14 Jan 2025 17:51:23 -0600 Subject: [PATCH 35/50] encapsulate all matching functions into a single findBestMatch entry point. --- src/idiomorph.js | 197 ++++++++++++++++++++++++++--------------------- test/fidelity.js | 7 ++ 2 files changed, 115 insertions(+), 89 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 8539edc..1a90e96 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -268,10 +268,12 @@ var Idiomorph = (function () { const morphedNodes = [...newParent.childNodes].map((newChild) => { // once we reach the end of the old parent content skip to the end and insert the rest if (insertionPoint && insertionPoint != endPoint) { - const bestMatch = - findIdSetMatch(newChild, insertionPoint, endPoint, ctx) || - findSoftMatch(newChild, insertionPoint, endPoint, ctx); - + const bestMatch = findBestMatch( + newChild, + insertionPoint, + endPoint, + ctx, + ); if (bestMatch) { // if the node to morph is not at the insertion point then delete up to it if (bestMatch !== insertionPoint) { @@ -338,102 +340,119 @@ var Idiomorph = (function () { } } - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {boolean} - */ - function isIdSetMatch(oldNode, newNode, ctx) { - if ( - oldNode instanceof Element && - newNode instanceof Element && - oldNode.tagName === newNode.tagName - ) { - if (oldNode.id !== "" && oldNode.id === newNode.id) { - return true; - } else { - return getIdIntersectionCount(oldNode, newNode, ctx) > 0; - } + //============================================================================= + // Matching Functions + //============================================================================= + const findBestMatch = (function () { + /** + * Scans forward from the insertionPoint to the endPoint looking for a match + * for the newChild. It looks for an id set match first, then a soft match. + * @param {Node} newChild + * @param {Node | null} insertionPoint + * @param {Node | null} endPoint + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function findBestMatch(newChild, insertionPoint, endPoint, ctx) { + return ( + findIdSetMatch(newChild, insertionPoint, endPoint, ctx) || + findSoftMatch(newChild, insertionPoint, endPoint, ctx) + ); } - return false; - } - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @returns {boolean} - */ - function isSoftMatch(oldNode, newNode) { - // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that - // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing - if ( - /** @type {Element} */ (oldNode).id && - /** @type {Element} */ (oldNode).id !== - /** @type {Element} */ (newNode).id - ) { - return false; + /** + * + * @param {Node} oldNode + * @param {Node} newNode + * @param {MorphContext} ctx + * @returns {boolean} + */ + function isIdSetMatch(oldNode, newNode, ctx) { + return oldNode instanceof Element && + isSoftMatch(oldNode, newNode) && + getIdIntersectionCount(oldNode, newNode, ctx) > 0; } - return ( - oldNode.nodeType === newNode.nodeType && - /** @type {Element} */ (oldNode).tagName === - /** @type {Element} */ (newNode).tagName - ); - } - /** - * Scans forward from the insertionPoint in the old parent looking for a potential id match - * for the newChild. - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { - const newChildPotentialIdCount = getPersistentIdNodeCount(ctx, newChild); - - // only search forward if there is a possibility of an id match - if (newChildPotentialIdCount > 0) { - let potentialMatch = insertionPoint; - while (potentialMatch && potentialMatch != endPoint) { - // If we have an id match, return the current potential match - if (isIdSetMatch(potentialMatch, newChild, ctx)) { - return potentialMatch; + /** + * + * @param {Node} oldNode + * @param {Node} newNode + * @returns {boolean} + */ + function isSoftMatch(oldNode, newNode) { + // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that + // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing + if ( + /** @type {Element} */ (oldNode).id && + /** @type {Element} */ (oldNode).id !== + /** @type {Element} */ (newNode).id + ) { + return false; + } + return ( + oldNode.nodeType === newNode.nodeType && + /** @type {Element} */ (oldNode).tagName === + /** @type {Element} */ (newNode).tagName + ); + } + + /** + * Scans forward from the insertionPoint in the old parent looking for a potential id match + * for the newChild. + * @param {Node} newChild + * @param {Node | null} insertionPoint + * @param {Node | null} endPoint + * @param {MorphContext} ctx + * @returns {Node | null} + */ + function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { + const newChildPotentialIdCount = getPersistentIdNodeCount( + ctx, + newChild, + ); + + // only search forward if there is a possibility of an id match + if (newChildPotentialIdCount > 0) { + let potentialMatch = insertionPoint; + while (potentialMatch && potentialMatch != endPoint) { + // If we have an id match, return the current potential match + if (isIdSetMatch(potentialMatch, newChild, ctx)) { + return potentialMatch; + } + potentialMatch = potentialMatch.nextSibling; } - potentialMatch = potentialMatch.nextSibling; } + return null; } - return null; - } - /** - * Scans forward from the insertionPoint in the old parent looking for a potential soft match - * for the newChild. - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {null | Node} - */ - function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { - let potentialSoftMatch = insertionPoint; - let nextSibling = newChild.nextSibling; - - while (potentialSoftMatch && potentialSoftMatch != endPoint) { - // the current potential soft match has a id set match with the remaining new - // content so leave this one for the future - if (!hasPersistentIdNodes(ctx, potentialSoftMatch)) { - if (isSoftMatch(potentialSoftMatch, newChild)) { - return potentialSoftMatch; + /** + * Scans forward from the insertionPoint in the old parent looking for a potential soft match + * for the newChild. + * @param {Node} newChild + * @param {Node | null} insertionPoint + * @param {Node | null} endPoint + * @param {MorphContext} ctx + * @returns {null | Node} + */ + function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { + let potentialSoftMatch = insertionPoint; + let nextSibling = newChild.nextSibling; + + while (potentialSoftMatch && potentialSoftMatch != endPoint) { + // the current potential soft match has a id set match with the remaining new + // content so leave this one for the future + if (!hasPersistentIdNodes(ctx, potentialSoftMatch)) { + if (isSoftMatch(potentialSoftMatch, newChild)) { + return potentialSoftMatch; + } } + potentialSoftMatch = potentialSoftMatch.nextSibling; } - potentialSoftMatch = potentialSoftMatch.nextSibling; + return null; } - return null; - } + + return findBestMatch; + })(); //============================================================================= // DOM Manipulation Functions diff --git a/test/fidelity.js b/test/fidelity.js index 410c8a3..8f1ed0f 100644 --- a/test/fidelity.js +++ b/test/fidelity.js @@ -81,6 +81,13 @@ describe("Tests to ensure that idiomorph merges properly", function () { testFidelity("
", "

hello you

"); }); + it("moves a node from the future", function () { + testFidelity( + `
`, + `
`, + ); + }); + it("issue https://github.com/bigskysoftware/idiomorph/issues/11", function () { let el1 = make('
'); From 9c664a299bf48f61cbf65fbc9951a6fcaffbf27f Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 11:59:14 -0600 Subject: [PATCH 36/50] minor cleanup. --- src/idiomorph.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 1a90e96..d017780 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -1059,13 +1059,11 @@ var Idiomorph = (function () { function populateIdMapForNode(nodeParent, nodes, persistentIds, idMap) { for (const elt of nodes) { if (persistentIds.has(elt.id)) { - /** - * @type {Element|null} - */ + /** @type {Element|null} */ let current = elt; // walk up the parent hierarchy of that element, adding the id // of element to the parent's id set - while (current !== nodeParent && current != null) { + while (current && current !== nodeParent) { let idSet = idMap.get(current); // if the id set doesn't exist, create it and insert it in the map if (idSet == null) { @@ -1097,7 +1095,7 @@ var Idiomorph = (function () { for (const oldElt of oldElts) { const id = oldElt.id; // if already in map then log duplicates to be skipped - if (oldIdMap.get(id)) { + if (oldIdMap.has(id)) { dupSet.add(id); } else { oldIdMap.set(id, oldElt.tagName); From 1567f80e7c77c35ba624f7ba46fcb9270f44891b Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 12:11:06 -0600 Subject: [PATCH 37/50] some minor contortions to get us back to 100% coverage. --- src/idiomorph.js | 6 ++++-- test/retain-hidden-state.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index d017780..68eed77 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -577,8 +577,10 @@ var Idiomorph = (function () { * @returns {number} */ function getIdIntersectionCount(oldNode, newNode, ctx) { - let oldSet = ctx.idMap.get(oldNode) || EMPTY_SET; - let newSet = ctx.idMap.get(newNode) || EMPTY_SET; + let oldSet = ctx.idMap.get(oldNode); + let newSet = ctx.idMap.get(newNode); + + if(!newSet || !oldSet) return 0 let matchCount = 0; for (const id of oldSet) { diff --git a/test/retain-hidden-state.js b/test/retain-hidden-state.js index 560c08a..4895717 100644 --- a/test/retain-hidden-state.js +++ b/test/retain-hidden-state.js @@ -296,6 +296,36 @@ describe("Hidden state preservation tests", function () { states.should.eql([true, true]); }); + it("preserves all non-attribute element state and innerHTML morphStyle when morphing to two top level nodes with nesting", function () { + getWorkArea().innerHTML = ` +
+ +
+
+
+ +
+ `; + document.getElementById("first").indeterminate = true; + document.getElementById("second").indeterminate = true; + + let finalSrc = ` +
+ +
+
+ +
+ `; + Idiomorph.morph(getWorkArea(), finalSrc, { morphStyle: "innerHTML" }); + + getWorkArea().innerHTML.should.equal(finalSrc); + const states = Array.from(getWorkArea().querySelectorAll("input")).map( + (e) => e.indeterminate, + ); + states.should.eql([true, true]); + }); + it("preserves all non-attribute element state when wrapping element changes tag", function () { // just changing the type from div to span of the wrapper causes softmatch to fail so it abandons all hope // of morphing and just inserts the node so we need to check this still handles preserving state here. From 596fe559af82079de5bfd3b3b1bc22af2f5d8819 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 13:14:19 -0600 Subject: [PATCH 38/50] merge findIdSetMatch and findSoftMatch so that we only iterate once, not twice. --- src/idiomorph.js | 94 ++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 68eed77..a0be320 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -354,10 +354,32 @@ var Idiomorph = (function () { * @returns {Node | null} */ function findBestMatch(newChild, insertionPoint, endPoint, ctx) { - return ( - findIdSetMatch(newChild, insertionPoint, endPoint, ctx) || - findSoftMatch(newChild, insertionPoint, endPoint, ctx) - ); + const newChildHasPersistentIds = hasPersistentIdNodes(ctx, newChild); + + let softMatch = null; + let cursor = insertionPoint; + while (cursor && cursor != endPoint) { + // soft matching is a prerequisite for hard matching + if (isSoftMatch(cursor, newChild)) { + // if there is a possibility of an id match + if (newChildHasPersistentIds) { + if (isIdSetMatch(cursor, newChild, ctx)) { + return cursor; // found a hard match, we're done! + } + } + + // we haven't yet saved the soft match fallback + if (!softMatch) { + // the current soft match will hard match something else in the future, leave it + if (!hasPersistentIdNodes(ctx, cursor)) { + softMatch = cursor; // save this as the fallback if we don't find a hard match + } + } + } + cursor = cursor.nextSibling; + } + + return softMatch; } /** @@ -368,9 +390,10 @@ var Idiomorph = (function () { * @returns {boolean} */ function isIdSetMatch(oldNode, newNode, ctx) { - return oldNode instanceof Element && - isSoftMatch(oldNode, newNode) && - getIdIntersectionCount(oldNode, newNode, ctx) > 0; + return ( + oldNode instanceof Element && + getIdIntersectionCount(oldNode, newNode, ctx) > 0 + ); } /** @@ -396,61 +419,6 @@ var Idiomorph = (function () { ); } - /** - * Scans forward from the insertionPoint in the old parent looking for a potential id match - * for the newChild. - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {Node | null} - */ - function findIdSetMatch(newChild, insertionPoint, endPoint, ctx) { - const newChildPotentialIdCount = getPersistentIdNodeCount( - ctx, - newChild, - ); - - // only search forward if there is a possibility of an id match - if (newChildPotentialIdCount > 0) { - let potentialMatch = insertionPoint; - while (potentialMatch && potentialMatch != endPoint) { - // If we have an id match, return the current potential match - if (isIdSetMatch(potentialMatch, newChild, ctx)) { - return potentialMatch; - } - potentialMatch = potentialMatch.nextSibling; - } - } - return null; - } - - /** - * Scans forward from the insertionPoint in the old parent looking for a potential soft match - * for the newChild. - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint - * @param {MorphContext} ctx - * @returns {null | Node} - */ - function findSoftMatch(newChild, insertionPoint, endPoint, ctx) { - let potentialSoftMatch = insertionPoint; - let nextSibling = newChild.nextSibling; - - while (potentialSoftMatch && potentialSoftMatch != endPoint) { - // the current potential soft match has a id set match with the remaining new - // content so leave this one for the future - if (!hasPersistentIdNodes(ctx, potentialSoftMatch)) { - if (isSoftMatch(potentialSoftMatch, newChild)) { - return potentialSoftMatch; - } - } - potentialSoftMatch = potentialSoftMatch.nextSibling; - } - return null; - } - return findBestMatch; })(); @@ -580,7 +548,7 @@ var Idiomorph = (function () { let oldSet = ctx.idMap.get(oldNode); let newSet = ctx.idMap.get(newNode); - if(!newSet || !oldSet) return 0 + if (!newSet || !oldSet) return 0; let matchCount = 0; for (const id of oldSet) { From 517ca8054df6f611d6b74b2f9bf98cdb86b844c1 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 13:40:08 -0600 Subject: [PATCH 39/50] now that we have one loop, we can simplify further. --- src/idiomorph.js | 51 +++++++++++++++--------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index a0be320..7351ac2 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -354,25 +354,22 @@ var Idiomorph = (function () { * @returns {Node | null} */ function findBestMatch(newChild, insertionPoint, endPoint, ctx) { - const newChildHasPersistentIds = hasPersistentIdNodes(ctx, newChild); - let softMatch = null; + let cursor = insertionPoint; while (cursor && cursor != endPoint) { - // soft matching is a prerequisite for hard matching + // soft matching is a prerequisite for id set matching if (isSoftMatch(cursor, newChild)) { - // if there is a possibility of an id match - if (newChildHasPersistentIds) { - if (isIdSetMatch(cursor, newChild, ctx)) { - return cursor; // found a hard match, we're done! - } + if (getIdIntersectionCount(cursor, newChild, ctx) > 0) { + return cursor; // found an id set match, we're done! } // we haven't yet saved the soft match fallback if (!softMatch) { // the current soft match will hard match something else in the future, leave it if (!hasPersistentIdNodes(ctx, cursor)) { - softMatch = cursor; // save this as the fallback if we don't find a hard match + // save this as the fallback if we get through the loop without finding a hard match + softMatch = cursor; } } } @@ -382,20 +379,6 @@ var Idiomorph = (function () { return softMatch; } - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {boolean} - */ - function isIdSetMatch(oldNode, newNode, ctx) { - return ( - oldNode instanceof Element && - getIdIntersectionCount(oldNode, newNode, ctx) > 0 - ); - } - /** * * @param {Node} oldNode @@ -403,19 +386,17 @@ var Idiomorph = (function () { * @returns {boolean} */ function isSoftMatch(oldNode, newNode) { - // ok to cast: if one is not element, `id` or `tagName` will be undefined and we'll compare that - // If oldNode has an `id` with possible state and it doesn't match newNode.id then avoid morphing - if ( - /** @type {Element} */ (oldNode).id && - /** @type {Element} */ (oldNode).id !== - /** @type {Element} */ (newNode).id - ) { - return false; - } + // ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that. + const oldElt = /** @type {Element} */ (oldNode); + const newElt = /** @type {Element} */ (newNode); + return ( - oldNode.nodeType === newNode.nodeType && - /** @type {Element} */ (oldNode).tagName === - /** @type {Element} */ (newNode).tagName + oldElt.nodeType === newElt.nodeType && + oldElt.tagName === newElt.tagName && + // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing. + // We'll still match an anonymous node with an IDed newElt, though, because if it got this far, + // its not persistent, and new nodes can't have any hidden state. + (!oldElt.id || oldElt.id === newElt.id) ); } From f84c66a484c4f1b465b97e6924f36bb950f6a83d Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 14:20:30 -0600 Subject: [PATCH 40/50] perf: go back to calculating outerHTML morphed nodes manually. This is 2n fewer array allocations, where n is the number of DOM nodes with children. --- src/idiomorph.js | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 7351ac2..5cd0ca6 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -157,30 +157,44 @@ var Idiomorph = (function () { const newNode = normalizeParent(newContent); const ctx = createMorphContext(oldNode, newNode, config); - return saveAndRestoreFocus(ctx, () => { + const morphedNodes = saveAndRestoreFocus(ctx, () => { return withHeadBlocking( ctx, oldNode, newNode, /** @param {MorphContext} ctx */ (ctx) => { - let morphedNodes; if (ctx.morphStyle === "innerHTML") { - morphedNodes = morphChildren(ctx, oldNode, newNode); + morphChildren(ctx, oldNode, newNode); + return Array.from(oldNode.childNodes); } else { // outerHTML - morphedNodes = morphChildren( + const oldParent = normalizeParent(oldNode); + + // basis for calulating which nodes were morphed + // since there maybe unmorphed sibling nodes + let childNodes = Array.from(oldParent.childNodes); + const index = childNodes.indexOf(oldNode); + // how many elements are to the right of the oldNode + const rightMargin = childNodes.length - (index + 1); + + morphChildren( ctx, - normalizeParent(oldNode), + oldParent, newNode, oldNode, oldNode.nextSibling, ); + + // rebuild childNodes + childNodes = Array.from(oldParent.childNodes); + return childNodes.slice(index, childNodes.length - rightMargin); } - ctx.pantry.remove(); - return morphedNodes; }, ); }); + + ctx.pantry.remove(); + return morphedNodes; } /** @@ -243,7 +257,6 @@ var Idiomorph = (function () { * @param {Element} newParent the parent element of the new content * @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child) * @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child) - * @returns {Node[]} */ function morphChildren( ctx, @@ -265,7 +278,7 @@ var Idiomorph = (function () { insertionPoint ||= oldParent.firstChild; // run through all the new content - const morphedNodes = [...newParent.childNodes].map((newChild) => { + for (const newChild of newParent.childNodes) { // once we reach the end of the old parent content skip to the end and insert the rest if (insertionPoint && insertionPoint != endPoint) { const bestMatch = findBestMatch( @@ -281,7 +294,7 @@ var Idiomorph = (function () { } morphNode(bestMatch, newChild, ctx); insertionPoint = bestMatch.nextSibling; - return bestMatch; + continue; } } @@ -295,12 +308,12 @@ var Idiomorph = (function () { ctx, ); morphNode(movedChild, newChild, ctx); - return movedChild; + continue; } // last resort: insert the new node from scratch - return createNode(oldParent, newChild, insertionPoint, ctx); - }); + createNode(oldParent, newChild, insertionPoint, ctx); + } // remove any remaining old nodes that didn't match up with new content while (insertionPoint && insertionPoint != endPoint) { @@ -308,8 +321,6 @@ var Idiomorph = (function () { insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } - - return morphedNodes.filter((e) => e != null); } /** From 5ce43e94ea953ee03288768837b8668733429b30 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 14:30:47 -0600 Subject: [PATCH 41/50] extract morphOuterHTML. --- src/idiomorph.js | 54 +++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 5cd0ca6..91f3f40 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -167,27 +167,7 @@ var Idiomorph = (function () { morphChildren(ctx, oldNode, newNode); return Array.from(oldNode.childNodes); } else { - // outerHTML - const oldParent = normalizeParent(oldNode); - - // basis for calulating which nodes were morphed - // since there maybe unmorphed sibling nodes - let childNodes = Array.from(oldParent.childNodes); - const index = childNodes.indexOf(oldNode); - // how many elements are to the right of the oldNode - const rightMargin = childNodes.length - (index + 1); - - morphChildren( - ctx, - oldParent, - newNode, - oldNode, - oldNode.nextSibling, - ); - - // rebuild childNodes - childNodes = Array.from(oldParent.childNodes); - return childNodes.slice(index, childNodes.length - rightMargin); + return morphOuterHTML(ctx, oldNode, newNode); } }, ); @@ -197,6 +177,38 @@ var Idiomorph = (function () { return morphedNodes; } + /** + * Morph just the outerHTML of the oldNode to the newContent + * We have to be careful because the oldNode could have siblings which need to be untouched + * @param {MorphContext} ctx + * @param {Element} oldNode + * @param {Element} newNode + * @returns {Node[]} + */ + function morphOuterHTML(ctx, oldNode, newNode) { + const oldParent = normalizeParent(oldNode); + + // basis for calulating which nodes were morphed + // since there may be unmorphed sibling nodes + let childNodes = Array.from(oldParent.childNodes); + const index = childNodes.indexOf(oldNode); + // how many elements are to the right of the oldNode + const rightMargin = childNodes.length - (index + 1); + + morphChildren( + ctx, + oldParent, + newNode, + // these two optional params are the secret sauce + oldNode, // start point for iteration + oldNode.nextSibling, // end point for iteration + ); + + // return just the morphed nodes + childNodes = Array.from(oldParent.childNodes); + return childNodes.slice(index, childNodes.length - rightMargin); + } + /** * @param {MorphContext} ctx * @param {Function} fn From 2a5e23137fe2e7449d5a84cb086854e78292e40b Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 15:07:13 -0600 Subject: [PATCH 42/50] fail ci if we fall short of 100% coverage, and rename some npm tasks. --- .github/workflows/ci.yml | 16 +++++++++++++++- TESTING.md | 4 ++-- package-lock.json | 10 ++++++++++ package.json | 6 ++++-- test/lib/ensure-full-coverage.js | 16 ++++++++++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 test/lib/ensure-full-coverage.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b81520..8025b6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,21 @@ jobs: - name: Install dependencies run: npm install - name: Run tests - run: npm run test-move-before + run: npm run test:move-before + + coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Check code coverage + run: npm run test:coverage typecheck: runs-on: ubuntu-latest diff --git a/TESTING.md b/TESTING.md index aa95604..7d49e07 100644 --- a/TESTING.md +++ b/TESTING.md @@ -27,7 +27,7 @@ This will run the tests using Playwright’s headless browser setup across Chrom To run all tests against Chrome with experimental `moveBefore` support added, execute: ```bash -npm run test-move-before +npm run test:move-before ``` This will start headless Chrome in a new profile with the `atomic-move` experimental flag set. This runs in a separate job in CI. @@ -50,7 +50,7 @@ This runs all the tests in the browser using Mocha instead of web-test-runner fo If you really want to open web-test-runner in headed mode, you can run: ```bash -npm run debug +npm run test:debug ``` This will start the server, and open the test runner in a browser. From there you can choose a test file to run. diff --git a/package-lock.json b/package-lock.json index 0712ba2..6fb4028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "chromedriver": "^131.0.5", "fs-extra": "^9.1.0", "htmx.org": "1.9.9", + "lcov-parse": "^1.0.0", "mocha": "^11.0.1", "prettier": "^3.4.2", "sinon": "^9.2.4", @@ -2843,6 +2844,15 @@ "ms": "^2.1.1" } }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true, + "bin": { + "lcov-parse": "bin/cli.js" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "dev": true, diff --git a/package.json b/package.json index eb20c3e..6e84758 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,9 @@ "unpkg": "dist/idiomorph.min.js", "scripts": { "test": "web-test-runner", - "debug": "web-test-runner --manual --open", - "test-move-before": "USE_MOVE_BEFORE=1 web-test-runner", + "test:coverage": "web-test-runner && node test/lib/ensure-full-coverage.js", + "test:debug": "web-test-runner --manual --open", + "test:move-before": "USE_MOVE_BEFORE=1 web-test-runner", "ci": "web-test-runner --fail-only --playwright --browsers chromium firefox webkit", "perf": "node perf/runner.js", "amd": "(echo \"define(() => {\n\" && cat src/idiomorph.js && echo \"\nreturn Idiomorph});\") > dist/idiomorph.amd.js", @@ -46,6 +47,7 @@ "chromedriver": "^131.0.5", "fs-extra": "^9.1.0", "htmx.org": "1.9.9", + "lcov-parse": "^1.0.0", "mocha": "^11.0.1", "prettier": "^3.4.2", "sinon": "^9.2.4", diff --git a/test/lib/ensure-full-coverage.js b/test/lib/ensure-full-coverage.js new file mode 100644 index 0000000..7c77fd0 --- /dev/null +++ b/test/lib/ensure-full-coverage.js @@ -0,0 +1,16 @@ +const lcovParse = require("lcov-parse"); + +lcovParse("coverage/lcov.info", (err, data) => { + if (err) { + console.error("Error parsing lcov file:", err); + process.exit(1); + } + + data.forEach((record) => { + ["lines", "functions", "branches"].forEach((type) => { + if(record[type].hit !== record[type].found) { + process.exit(1); + } + }); + }); +}); From 1f38bdbd9bf059247548eb18281231b785c4aeaa Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Wed, 15 Jan 2025 15:48:49 -0600 Subject: [PATCH 43/50] try publishing coverage report as an artifact. --- .github/workflows/ci.yml | 10 +++++++++- package.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8025b6c..eed56e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,16 @@ jobs: cache: 'npm' - name: Install dependencies run: npm install - - name: Check code coverage + - name: Install browsers + run: npx playwright install --with-deps + - name: Run tests run: npm run test:coverage + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ typecheck: runs-on: ubuntu-latest diff --git a/package.json b/package.json index 6e84758..230f7dc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "unpkg": "dist/idiomorph.min.js", "scripts": { "test": "web-test-runner", - "test:coverage": "web-test-runner && node test/lib/ensure-full-coverage.js", + "test:coverage": "npm run ci && node test/lib/ensure-full-coverage.js", "test:debug": "web-test-runner --manual --open", "test:move-before": "USE_MOVE_BEFORE=1 web-test-runner", "ci": "web-test-runner --fail-only --playwright --browsers chromium firefox webkit", From c06a518cf63ce0954d1e03acb7b6c7da3b9a20ef Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 17 Jan 2025 02:03:43 +1300 Subject: [PATCH 44/50] update target to es2016 to support includes --- package-lock.json | 6202 ++++++++++++++++++++++----------------------- tsconfig.json | 2 +- 2 files changed, 2969 insertions(+), 3235 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6fb4028..aa73100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,9 @@ }, "node_modules/@babel/code-frame": { "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -38,18 +39,111 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", "dev": true, - "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bazel/runfiles": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz", + "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==", + "dev": true + }, "node_modules/@hapi/bourne": { "version": "3.0.0", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "dev": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -68,18 +162,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "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", @@ -92,78 +174,66 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -171,8 +241,9 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -183,16 +254,18 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -213,8 +286,9 @@ }, "node_modules/@puppeteer/browsers": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", @@ -232,10 +306,110 @@ "node": ">=18" } }, + "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/@puppeteer/browsers/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", @@ -257,8 +431,9 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", @@ -278,8 +453,9 @@ }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -287,58 +463,297 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "cpu": [ - "x64" + "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "linux" + "android" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "linux" + "android" ] }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sinonjs/commons/node_modules/type-detect": { - "version": "4.0.8", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sinonjs/fake-timers": { - "version": "6.0.1", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@sinonjs/samsam": { + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", @@ -347,8 +762,21 @@ }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, - "license": "(Unlicense OR Apache-2.0)" + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } }, "node_modules/@testim/chrome-version": { "version": "1.1.4", @@ -358,26 +786,39 @@ }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true }, "node_modules/@types/accepts": { "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/babel__code-frame": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz", + "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", + "dev": true + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, - "license": "MIT" + "dependencies": { + "@babel/types": "^7.0.0" + } }, "node_modules/@types/body-parser": { "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, - "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -385,8 +826,9 @@ }, "node_modules/@types/co-body": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*" @@ -394,31 +836,36 @@ }, "node_modules/@types/command-line-args": { "version": "5.2.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "dev": true }, "node_modules/@types/connect": { "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/content-disposition": { "version": "0.5.8", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "dev": true }, "node_modules/@types/convert-source-map": { "version": "2.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/convert-source-map/-/convert-source-map-2.0.3.tgz", + "integrity": "sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==", + "dev": true }, "node_modules/@types/cookies": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/connect": "*", "@types/express": "*", @@ -428,18 +875,30 @@ }, "node_modules/@types/debounce": { "version": "1.2.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz", + "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", + "dev": true }, "node_modules/@types/estree": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/execa": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/execa/-/execa-0.9.0.tgz", + "integrity": "sha512-mgfd93RhzjYBUHHV532turHC2j4l/qxsF/PbfDmprHDEUHmNZGlDn1CEsulGK3AfsPdhkWzZQT/S/k0UGhLGsA==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/express": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -448,9 +907,10 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -460,44 +920,57 @@ }, "node_modules/@types/http-assert": { "version": "1.5.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true }, "node_modules/@types/http-errors": { "version": "2.0.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, - "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/keygrip": { "version": "1.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true }, "node_modules/@types/koa": { "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "dev": true, - "license": "MIT", "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -511,49 +984,57 @@ }, "node_modules/@types/koa-compose": { "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", "dev": true, - "license": "MIT", "dependencies": { "@types/koa": "*" } }, "node_modules/@types/mime": { "version": "1.3.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/node": { - "version": "22.10.2", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, "node_modules/@types/parse5": { "version": "6.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true }, "node_modules/@types/qs": { - "version": "6.9.17", - "dev": true, - "license": "MIT" + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/resolve": { "version": "1.20.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true }, "node_modules/@types/send": { "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, - "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -561,8 +1042,9 @@ }, "node_modules/@types/serve-static": { "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -571,27 +1053,30 @@ }, "node_modules/@types/ws": { "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yauzl": { "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, "node_modules/@web/browser-logs": { - "version": "0.4.0", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", + "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", "dev": true, - "license": "MIT", "dependencies": { - "errorstacks": "^2.2.0" + "errorstacks": "^2.4.1" }, "engines": { "node": ">=18.0.0" @@ -599,16 +1084,18 @@ }, "node_modules/@web/config-loader": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.2.tgz", + "integrity": "sha512-Vrjv/FexBGmAdnCYpJKLHX1dfT1UaUdvHmX1JRaWos9OvDf/tFznYJ5SpJwww3Rl87/ewvLSYG7kfsMqEAsizQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.0.0" } }, "node_modules/@web/dev-server": { "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", + "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/command-line-args": "^5.0.0", @@ -634,9 +1121,10 @@ } }, "node_modules/@web/dev-server-core": { - "version": "0.7.4", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", + "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", "dev": true, - "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", @@ -663,8 +1151,9 @@ }, "node_modules/@web/dev-server-rollup": { "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz", + "integrity": "sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/plugin-node-resolve": "^15.0.1", "@web/dev-server-core": "^0.7.2", @@ -679,8 +1168,9 @@ }, "node_modules/@web/parse5-utils": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", + "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", "dev": true, - "license": "MIT", "dependencies": { "@types/parse5": "^6.0.1", "parse5": "^6.0.1" @@ -691,8 +1181,9 @@ }, "node_modules/@web/test-runner": { "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@web/test-runner/-/test-runner-0.19.0.tgz", + "integrity": "sha512-qLUupi88OK1Kl52cWPD/2JewUCRUxYsZ1V1DyLd05P7u09zCdrUYrtkB/cViWyxlBe/TOvqkSNpcTv6zLJ9GoA==", "dev": true, - "license": "MIT", "dependencies": { "@web/browser-logs": "^0.4.0", "@web/config-loader": "^0.3.0", @@ -721,8 +1212,9 @@ }, "node_modules/@web/test-runner-chrome": { "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-chrome/-/test-runner-chrome-0.17.0.tgz", + "integrity": "sha512-Il5N9z41NKWCrQM1TVgRaDWWYoJtG5Ha4fG+cN1MWL2OlzBS4WoOb4lFV3EylZ7+W3twZOFr1zy2Rx61yDYd/A==", "dev": true, - "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0", "@web/test-runner-coverage-v8": "^0.8.0", @@ -736,8 +1228,9 @@ }, "node_modules/@web/test-runner-commands": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz", + "integrity": "sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==", "dev": true, - "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0", "mkdirp": "^1.0.4" @@ -748,8 +1241,9 @@ }, "node_modules/@web/test-runner-core": { "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/babel__code-frame": "^7.0.2", @@ -784,8 +1278,9 @@ }, "node_modules/@web/test-runner-coverage-v8": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz", + "integrity": "sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==", "dev": true, - "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0", "istanbul-lib-coverage": "^3.0.0", @@ -799,8 +1294,9 @@ }, "node_modules/@web/test-runner-mocha": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz", + "integrity": "sha512-ZL9F6FXd0DBQvo/h/+mSfzFTSRVxzV9st/AHhpgABtUtV/AIpVE9to6+xdkpu6827kwjezdpuadPfg+PlrBWqQ==", "dev": true, - "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0" }, @@ -810,8 +1306,9 @@ }, "node_modules/@web/test-runner-playwright": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.0.tgz", + "integrity": "sha512-s+f43DSAcssKYVOD9SuzueUcctJdHzq1by45gAnSCKa9FQcaTbuYe8CzmxA21g+NcL5+ayo4z+MA9PO4H+PssQ==", "dev": true, - "license": "MIT", "dependencies": { "@web/test-runner-core": "^0.13.0", "@web/test-runner-coverage-v8": "^0.8.0", @@ -823,8 +1320,9 @@ }, "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, - "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -835,12 +1333,29 @@ }, "node_modules/agent-base": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -850,10 +1365,40 @@ "node": ">=6" } }, + "node_modules/ansi-escape-sequences": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-6.2.4.tgz", + "integrity": "sha512-2KJQAG1Nk4Iyu0dJENKXQJE9smEASrpu/E0F7LSnR72tQXngKGLqfRkHbkinjNct5vvAQY4BwQNt+4Tvg73nDQ==", + "dev": true, + "dependencies": { + "array-back": "^6.2.2" + }, + "engines": { + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/ansi-escape-sequences/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "dev": true, + "engines": { + "node": ">=12.17" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -865,17 +1410,22 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -907,32 +1457,36 @@ }, "node_modules/array-back": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/assertion-error": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/ast-types": { "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -942,24 +1496,27 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/async": { "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, - "license": "MIT", "dependencies": { "lodash": "^4.17.14" } }, "node_modules/async-mutex": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -972,8 +1529,9 @@ }, "node_modules/at-least-node": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, - "license": "ISC", "engines": { "node": ">= 4.0.0" } @@ -991,8 +1549,9 @@ }, "node_modules/b4a": { "version": "1.6.7", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true }, "node_modules/balanced-match": { "version": "1.0.2", @@ -1001,48 +1560,73 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.5.0", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", "dev": true, - "license": "Apache-2.0", "optional": true }, "node_modules/bare-fs": { - "version": "2.3.5", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", + "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", "dev": true, - "license": "Apache-2.0", "optional": true, "dependencies": { "bare-events": "^2.0.0", - "bare-path": "^2.0.0", + "bare-path": "^3.0.0", "bare-stream": "^2.0.0" + }, + "engines": { + "bare": ">=1.7.0" } }, "node_modules/bare-os": { - "version": "2.4.4", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", + "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", "dev": true, - "license": "Apache-2.0", - "optional": true + "optional": true, + "engines": { + "bare": ">=1.6.0" + } }, "node_modules/bare-path": { - "version": "2.1.3", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, - "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.6.1", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.4.tgz", + "integrity": "sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA==", "dev": true, - "license": "Apache-2.0", "optional": true, "dependencies": { "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -1057,13 +1641,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/basic-ftp": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -1091,8 +1675,9 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1108,6 +1693,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -1123,7 +1710,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1131,24 +1717,39 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cache-content-type": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", "dev": true, - "license": "MIT", "dependencies": { "mime-types": "^2.1.18", "ylru": "^1.2.0" @@ -1157,10 +1758,38 @@ "node": ">= 6.0.0" } }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1171,8 +1800,9 @@ }, "node_modules/call-bound": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -1186,8 +1816,9 @@ }, "node_modules/camelcase": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1197,8 +1828,9 @@ }, "node_modules/chai": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, - "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -1213,9 +1845,10 @@ } }, "node_modules/chai-dom": { - "version": "1.12.0", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/chai-dom/-/chai-dom-1.12.1.tgz", + "integrity": "sha512-tvz+D0PJue2VHXRec3udgP/OeeXBiePU3VH6JhEnHQJYzvNzR2nUvEykA9dXVS76JvaUENSOYH8Ufr0kZSnlCQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.12.0" }, @@ -1225,8 +1858,9 @@ }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1240,8 +1874,9 @@ }, "node_modules/chalk-template": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^4.1.2" }, @@ -1254,8 +1889,9 @@ }, "node_modules/check-error": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, - "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -1265,8 +1901,9 @@ }, "node_modules/chokidar": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -1279,8 +1916,9 @@ }, "node_modules/chrome-launcher": { "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -1318,8 +1956,9 @@ }, "node_modules/chromium-bidi": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" @@ -1330,8 +1969,9 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, - "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" }, @@ -1340,22 +1980,62 @@ } }, "node_modules/cliui": { - "version": "8.0.1", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", + "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1370,16 +2050,18 @@ }, "node_modules/clone": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/co": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, - "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -1387,8 +2069,9 @@ }, "node_modules/co-body": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", "dev": true, - "license": "MIT", "dependencies": { "@hapi/bourne": "^3.0.0", "inflation": "^2.0.0", @@ -1402,8 +2085,9 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1413,8 +2097,9 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1430,8 +2115,9 @@ }, "node_modules/command-line-args": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, - "license": "MIT", "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", @@ -1444,8 +2130,9 @@ }, "node_modules/command-line-usage": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "dev": true, - "license": "MIT", "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", @@ -1458,16 +2145,18 @@ }, "node_modules/command-line-usage/node_modules/array-back": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.17" } }, "node_modules/command-line-usage/node_modules/typical": { "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.17" } @@ -1480,8 +2169,9 @@ }, "node_modules/content-disposition": { "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -1491,21 +2181,24 @@ }, "node_modules/content-type": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/convert-source-map": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/cookies": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", "dev": true, - "license": "MIT", "dependencies": { "depd": "~2.0.0", "keygrip": "~1.1.0" @@ -1514,10 +2207,23 @@ "node": ">= 0.8" } }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1527,23 +2233,32 @@ "node": ">= 8" } }, + "node_modules/csv-stringify": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.2.tgz", + "integrity": "sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==", + "dev": true + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/debounce": { "version": "1.2.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true }, "node_modules/debug": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1568,10 +2283,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, - "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -1581,8 +2324,18 @@ }, "node_modules/deep-equal": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=4.0.0" + } }, "node_modules/deep-is": { "version": "0.1.4", @@ -1592,16 +2345,18 @@ }, "node_modules/deepmerge": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/default-gateway": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "execa": "^5.0.0" }, @@ -1609,18 +2364,29 @@ "node": ">= 10" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/degenerator": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, - "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -1641,29 +2407,33 @@ }, "node_modules/delegates": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/dependency-graph": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -1671,21 +2441,24 @@ }, "node_modules/devtools-protocol": { "version": "0.0.1367902", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "dev": true }, "node_modules/diff": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -1695,8 +2468,9 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -1712,62 +2486,80 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/end-of-stream": { "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/errorstacks": { "version": "2.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/errorstacks/-/errorstacks-2.4.1.tgz", + "integrity": "sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==", + "dev": true }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "dev": true, - "license": "MIT" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -1777,21 +2569,24 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-html": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -1801,8 +2596,9 @@ }, "node_modules/escodegen": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -1821,8 +2617,9 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -1830,8 +2627,9 @@ }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -1842,37 +2640,42 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { "version": "2.0.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/execa": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -1893,8 +2696,9 @@ }, "node_modules/extract-zip": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -1912,8 +2716,9 @@ }, "node_modules/extract-zip/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -1924,46 +2729,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "node_modules/fast-fifo": { "version": "1.3.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, + "node_modules/fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fastq": { "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fd-slicer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, - "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1973,8 +2805,9 @@ }, "node_modules/find-replace": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, - "license": "MIT", "dependencies": { "array-back": "^3.0.1" }, @@ -1982,6 +2815,22 @@ "node": ">=4.0.0" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -2053,18 +2902,29 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "engines": { + "node": ">= 14.17" + } + }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fs-extra": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, - "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -2076,9 +2936,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "optional": true, @@ -2091,43 +2951,47 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-func-name": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.2.6", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2136,10 +3000,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -2149,8 +3027,9 @@ }, "node_modules/get-uri": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, - "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -2182,8 +3061,9 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2206,10 +3086,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/globby": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2227,8 +3117,9 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2236,23 +3127,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2262,8 +3181,9 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -2276,8 +3196,9 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2296,18 +3217,21 @@ }, "node_modules/html-escaper": { "version": "2.0.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "node_modules/htmx.org": { "version": "1.9.9", - "dev": true, - "license": "BSD 2-Clause" + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.9.tgz", + "integrity": "sha512-PDEZU1me7UGLzQk98LyfLvwFgdtn9mrCVMmAxv1/UjshUnxsc+rouu+Ot2QfFZxsY4mBCoOed5nK7m9Nj2Tu7g==", + "dev": true }, "node_modules/http-assert": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", "dev": true, - "license": "MIT", "dependencies": { "deep-equal": "~1.0.1", "http-errors": "~1.8.0" @@ -2316,10 +3240,17 @@ "node": ">= 0.8" } }, - "node_modules/http-errors": { + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, - "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", @@ -2333,16 +3264,18 @@ }, "node_modules/http-errors/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -2351,10 +3284,24 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -2365,16 +3312,18 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -2384,6 +3333,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -2398,34 +3349,43 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/inflation": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-ip": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", "dev": true, - "license": "MIT", "dependencies": { "default-gateway": "^6.0.0", "ipaddr.js": "^1.9.1", @@ -2441,8 +3401,9 @@ }, "node_modules/ip-address": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, - "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -2453,16 +3414,18 @@ }, "node_modules/ip-regex": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ipaddr.js": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.10" } @@ -2481,8 +3444,9 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2495,8 +3459,9 @@ }, "node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, - "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -2509,26 +3474,32 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-generator-function": { - "version": "1.0.10", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2539,8 +3510,9 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2550,8 +3522,9 @@ }, "node_modules/is-ip": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", "dev": true, - "license": "MIT", "dependencies": { "ip-regex": "^4.0.0" }, @@ -2561,13 +3534,15 @@ }, "node_modules/is-module": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2581,10 +3556,29 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -2612,8 +3606,9 @@ }, "node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, - "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -2637,13 +3632,15 @@ }, "node_modules/isarray": { "version": "0.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true }, "node_modules/isbinaryfile": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.4.tgz", + "integrity": "sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 18.0.0" }, @@ -2653,21 +3650,24 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -2679,8 +3679,9 @@ }, "node_modules/istanbul-reports": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -2706,8 +3707,9 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2723,13 +3725,39 @@ }, "node_modules/jsbn": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "license": "MIT" + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true }, "node_modules/jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -2737,15 +3765,87 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jstat": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz", + "integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==", + "dev": true + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/just-extend": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dev": true, - "license": "MIT" + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } }, "node_modules/keygrip": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", "dev": true, - "license": "MIT", "dependencies": { "tsscmp": "1.0.6" }, @@ -2753,10 +3853,20 @@ "node": ">= 0.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/koa": { "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", @@ -2786,15 +3896,31 @@ "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" } }, + "node_modules/koa-bodyparser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.4.1.tgz", + "integrity": "sha512-kBH3IYPMb+iAXnrxIhXnW+gXV8OTzCu8VPDqvcDHW9SQrbkHmqPQtiZwrltNmSq6/lpipHnT7k7PsjlVD7kK0w==", + "dev": true, + "dependencies": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/koa-compose": { "version": "4.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true }, "node_modules/koa-convert": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", "dev": true, - "license": "MIT", "dependencies": { "co": "^4.6.0", "koa-compose": "^4.1.0" @@ -2805,16 +3931,74 @@ }, "node_modules/koa-etag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-etag/-/koa-etag-4.0.0.tgz", + "integrity": "sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==", "dev": true, - "license": "MIT", "dependencies": { "etag": "^1.8.1" } }, + "node_modules/koa-mount": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "dev": true, + "dependencies": { + "debug": "^4.0.1", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-node-resolve": { + "version": "1.0.0-pre.9", + "resolved": "https://registry.npmjs.org/koa-node-resolve/-/koa-node-resolve-1.0.0-pre.9.tgz", + "integrity": "sha512-WKgqe5TGVD6zuR3NrKnmbb/NNHIbWOCezQVqqnyQLdtLLXWgiothlUQT23S5qQGE0Z623jp6jxpMjvAqyrcZFQ==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/traverse": "^7.4.5", + "@types/babel__generator": "^7.6.1", + "@types/parse5": "^5.0.0", + "get-stream": "^5.1.0", + "parse5": "^5.1.0", + "resolve": "^1.11.0" + } + }, + "node_modules/koa-node-resolve/node_modules/@types/parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", + "dev": true + }, + "node_modules/koa-node-resolve/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/koa-node-resolve/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, "node_modules/koa-send": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.1", "http-errors": "^1.7.3", @@ -2826,8 +4010,9 @@ }, "node_modules/koa-static": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.1.0", "koa-send": "^5.0.0" @@ -2838,8 +4023,9 @@ }, "node_modules/koa-static/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2853,10 +4039,20 @@ "lcov-parse": "bin/cli.js" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", "dev": true, - "license": "Apache-2.0", "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" @@ -2864,31 +4060,99 @@ }, "node_modules/lighthouse-logger/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/lighthouse-logger/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT" + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true }, "node_modules/lodash.get": { "version": "4.4.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -2908,8 +4172,9 @@ }, "node_modules/log-update": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, - "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", @@ -2925,24 +4190,39 @@ }, "node_modules/loupe": { "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, - "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", "dev": true, - "license": "ISC", "engines": { "node": ">=16.14" } }, "node_modules/make-dir": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -2955,42 +4235,48 @@ }, "node_modules/marky": { "version": "1.2.5", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/merge-stream": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -3001,16 +4287,18 @@ }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -3020,12 +4308,25 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -3040,8 +4341,9 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3057,13 +4359,15 @@ }, "node_modules/mitt": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true }, "node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -3130,87 +4434,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3238,74 +4461,22 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ms": { "version": "2.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/nanocolors": { "version": "0.2.13", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", + "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==", + "dev": true }, "node_modules/nanoid": { "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -3313,7 +4484,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -3323,24 +4493,33 @@ }, "node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/netmask": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4.0" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node_modules/nise": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0", "@sinonjs/fake-timers": "^6.0.0", @@ -3358,10 +4537,23 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -3371,8 +4563,9 @@ }, "node_modules/object-inspect": { "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3382,8 +4575,9 @@ }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -3393,16 +4587,18 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, - "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -3415,12 +4611,15 @@ }, "node_modules/only": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", "dev": true }, "node_modules/open": { "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, - "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -3433,10 +4632,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, "node_modules/p-event": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", "dev": true, - "license": "MIT", "dependencies": { "p-timeout": "^3.1.0" }, @@ -3449,16 +4658,48 @@ }, "node_modules/p-finally": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-timeout": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "dev": true, - "license": "MIT", "dependencies": { "p-finally": "^1.0.0" }, @@ -3468,8 +4709,9 @@ }, "node_modules/pac-proxy-agent": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", "dev": true, - "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -3486,8 +4728,9 @@ }, "node_modules/pac-resolver": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, - "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -3502,39 +4745,59 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parse5": { "version": "6.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "1.11.1", @@ -3560,42 +4823,48 @@ }, "node_modules/path-to-regexp": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, - "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/pathval": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/pend": { "version": "1.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true }, "node_modules/picocolors": { "version": "1.1.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3603,73 +4872,296 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.49.1", + "node_modules/pkg-install": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-install/-/pkg-install-1.0.0.tgz", + "integrity": "sha512-UGI8bfhrDb1KN01RZ7Bq08GRQc8rmVjxQ2up0g4mUHPCYDTK1FzQ0PMmLOBCHg3yaIijZ2U3Fn9ofLa4N392Ug==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "@types/execa": "^0.9.0", + "@types/node": "^11.9.4", + "execa": "^1.0.0" } }, - "node_modules/playwright-core": { - "version": "1.49.1", + "node_modules/pkg-install/node_modules/@types/node": { + "version": "11.15.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.54.tgz", + "integrity": "sha512-1RWYiq+5UfozGsU6MwJyFX6BtktcT10XRjvcAQmskCtMcW3tPske88lM/nHv7BQG1w9KBXI1zPGuu5PnNCX14g==", + "dev": true + }, + "node_modules/pkg-install/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" }, "engines": { - "node": ">=18" + "node": ">=4.8" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/pkg-install/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6" } }, - "node_modules/portfinder": { - "version": "1.0.32", + "node_modules/pkg-install/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, - "license": "MIT", "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" + "pump": "^3.0.0" }, "engines": { - "node": ">= 0.12.0" + "node": ">=6" } }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", + "node_modules/pkg-install/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-install/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-install/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-install/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/pkg-install/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-install/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-install/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-4.0.0.tgz", + "integrity": "sha512-N4zdA4sfOe6yCv+ulPCmpnIBQ5I60xfhDr1otdBBhKte9QtEf3bhfrfkW7dTb+IQ0iEx4ZDzas0kc1o5rdWpYg==", + "dev": true, + "dependencies": { + "find-up": "^6.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-up/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" } }, "node_modules/portfinder/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -3679,8 +5171,9 @@ }, "node_modules/prettier": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -3691,18 +5184,26 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, "node_modules/progress": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/proxy-agent": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -3719,21 +5220,24 @@ }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/proxy-from-env": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "node_modules/pump": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3741,16 +5245,18 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/puppeteer-core": { "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", @@ -3765,8 +5271,9 @@ }, "node_modules/puppeteer-core/node_modules/ws": { "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -3784,11 +5291,12 @@ } }, "node_modules/qs": { - "version": "6.13.1", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3799,6 +5307,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -3813,13 +5323,25 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/queue-tick": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/randombytes": { "version": "2.1.0", @@ -3832,8 +5354,9 @@ }, "node_modules/raw-body": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3846,8 +5369,9 @@ }, "node_modules/raw-body/node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, - "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -3861,36 +5385,85 @@ }, "node_modules/raw-body/node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/readdirp": { - "version": "4.0.2", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", "url": "https://paulmillr.com/funding/" } }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, - "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -3906,10 +5479,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-path": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", "dev": true, - "license": "MIT", "dependencies": { "http-errors": "~1.6.2", "path-is-absolute": "1.0.1" @@ -3920,16 +5500,18 @@ }, "node_modules/resolve-path/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/resolve-path/node_modules/http-errors": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, - "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -3942,18 +5524,36 @@ }, "node_modules/resolve-path/node_modules/inherits": { "version": "2.0.3", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true }, "node_modules/resolve-path/node_modules/setprototypeof": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dev": true, - "license": "ISC" + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/restore-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, - "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -3964,17 +5564,19 @@ }, "node_modules/reusify": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rollup": { - "version": "4.29.1", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.6" }, @@ -3986,30 +5588,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -4025,13 +5629,14 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -4046,27 +5651,100 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "license": "MIT" + ] }, - "node_modules/semver": { - "version": "7.6.3", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/selenium-webdriver": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.27.0.tgz", + "integrity": "sha512-LkTJrNz5socxpPnWPODQ2bQ65eYx9JK+DQMYNihpTjMCqHwgWGYQnQTCAAche2W3ZP87alA+1zYPvgS8tHNzMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/SeleniumHQ" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/selenium" + } + ], + "dependencies": { + "@bazel/runfiles": "^6.3.1", + "jszip": "^3.10.1", + "tmp": "^0.2.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">= 14.21.0" + } + }, + "node_modules/selenium-webdriver/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, @@ -4074,15 +5752,23 @@ "randombytes": "^2.1.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, "node_modules/setprototypeof": { "version": "1.2.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4092,16 +5778,18 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -4118,8 +5806,9 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -4133,8 +5822,9 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -4150,8 +5840,9 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -4168,13 +5859,16 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/sinon": { "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "deprecated": "16.1.1", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.8.1", "@sinonjs/fake-timers": "^6.0.1", @@ -4190,24 +5884,27 @@ }, "node_modules/sinon/node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/slice-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -4222,8 +5919,9 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -4231,8 +5929,9 @@ }, "node_modules/socks": { "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, - "license": "MIT", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -4244,8 +5943,9 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -4257,29 +5957,52 @@ }, "node_modules/source-map": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true }, "node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/streamx": { "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", "dev": true, - "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", "queue-tick": "^1.0.1", @@ -4289,17 +6012,36 @@ "bare-events": "^2.2.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/string-width": { - "version": "4.2.3", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -4317,10 +6059,26 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4328,6 +6086,21 @@ "node": ">=8" } }, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -4341,10 +6114,29 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -4363,8 +6155,9 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4374,8 +6167,9 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4383,10 +6177,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/systeminformation": { + "version": "5.25.11", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.25.11.tgz", + "integrity": "sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==", + "dev": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/table-layout": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "dev": true, - "license": "MIT", "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" @@ -4397,16 +6234,59 @@ }, "node_modules/table-layout/node_modules/array-back": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.17" } }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tachometer": { "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tachometer/-/tachometer-0.7.1.tgz", + "integrity": "sha512-gOc7NfhriHamSFG9bBVzhB80V97jUoIPKz3QOO8vDma7hUwJSulShCUm+dS7qu2r+qH/dcvB1jFQnt/m2Z4O+w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "ansi-escape-sequences": "^6.0.1", "command-line-args": "^5.0.2", @@ -4441,2483 +6321,144 @@ "tachometer": "bin/tach.js" } }, - "node_modules/tachometer/node_modules/@babel/code-frame": { - "version": "7.26.2", + "node_modules/tachometer/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=4" } }, - "node_modules/tachometer/node_modules/@babel/generator": { - "version": "7.26.5", + "node_modules/tachometer/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/tachometer/node_modules/@babel/helper-string-parser": { - "version": "7.25.9", + "node_modules/tachometer/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=4" } }, - "node_modules/tachometer/node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", + "node_modules/tachometer/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "color-name": "1.1.3" } }, - "node_modules/tachometer/node_modules/@babel/parser": { - "version": "7.26.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tachometer/node_modules/@babel/template": { - "version": "7.25.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/tachometer/node_modules/@babel/traverse": { - "version": "7.26.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.5", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.5", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/tachometer/node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/@babel/types": { - "version": "7.26.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/tachometer/node_modules/@bazel/runfiles": { - "version": "6.3.1", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tachometer/node_modules/@hapi/bourne": { - "version": "3.0.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/tachometer/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tachometer/node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tachometer/node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tachometer/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/tachometer/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tachometer/node_modules/@types/babel__generator": { - "version": "7.6.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/tachometer/node_modules/@types/execa": { - "version": "0.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/tachometer/node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/@types/node": { - "version": "22.10.5", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/tachometer/node_modules/@types/parse5": { - "version": "5.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/accepts": { - "version": "1.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/ansi-escape-sequences": { - "version": "6.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "array-back": "^6.2.2" - }, - "engines": { - "node": ">=12.17" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, - "node_modules/tachometer/node_modules/ansi-escape-sequences/node_modules/array-back": { - "version": "6.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/tachometer/node_modules/ansi-regex": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/array-back": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/astral-regex": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/tachometer/node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/bytes": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/cache-content-type": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/tachometer/node_modules/cacheable-lookup": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tachometer/node_modules/cacheable-request": { - "version": "10.2.14", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tachometer/node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/call-bound": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/co": { - "version": "4.6.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/tachometer/node_modules/co-body": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@hapi/bourne": "^3.0.0", - "inflation": "^2.0.0", - "qs": "^6.5.2", - "raw-body": "^2.3.3", - "type-is": "^1.6.16" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tachometer/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/tachometer/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/command-line-args": { - "version": "5.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tachometer/node_modules/command-line-args/node_modules/array-back": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/command-line-args/node_modules/typical": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/command-line-usage": { - "version": "6.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tachometer/node_modules/content-disposition": { - "version": "0.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/content-type": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/cookies": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/copy-to": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/core-util-is": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/csv-stringify": { - "version": "6.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/debug": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/tachometer/node_modules/decompress-response": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/deep-equal": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/deep-extend": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tachometer/node_modules/defer-to-connect": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/tachometer/node_modules/delegates": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/destroy": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/tachometer/node_modules/dunder-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/tachometer/node_modules/ee-first": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/encodeurl": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/end-of-stream": { - "version": "1.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/tachometer/node_modules/es-define-property": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/es-object-atoms": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/escape-html": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/tachometer/node_modules/execa": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/execa/node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/tachometer/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/fast-uri": { - "version": "3.0.5", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/tachometer/node_modules/find-replace": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tachometer/node_modules/find-replace/node_modules/array-back": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/form-data-encoder": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/tachometer/node_modules/fresh": { - "version": "0.5.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/fs-extra": { - "version": "10.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/tachometer/node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/get-intrinsic": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "function-bind": "^1.1.2", - "get-proto": "^1.0.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/get-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/gopd": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/got": { - "version": "12.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/has-symbols": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/hasown": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/http-assert": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/http-cache-semantics": { - "version": "4.1.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/tachometer/node_modules/http-errors": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/http2-wrapper": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/tachometer/node_modules/iconv-lite": { - "version": "0.4.24", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/immediate": { - "version": "3.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/inflation": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/tachometer/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/is-core-module": { - "version": "2.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/is-generator-function": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/is-regex": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/is-stream": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/jsesc": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/tachometer/node_modules/jsonschema": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/tachometer/node_modules/jsonwebtoken": { - "version": "9.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/tachometer/node_modules/jstat": { - "version": "1.9.6", - "dev": true - }, - "node_modules/tachometer/node_modules/jszip": { - "version": "3.10.1", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/tachometer/node_modules/jwa": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/tachometer/node_modules/jws": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/tachometer/node_modules/keygrip": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/tachometer/node_modules/koa": { - "version": "2.15.3", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.9.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" - } - }, - "node_modules/tachometer/node_modules/koa-bodyparser": { - "version": "4.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "co-body": "^6.0.0", - "copy-to": "^2.0.1", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tachometer/node_modules/koa-compose": { - "version": "4.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/koa-convert": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/tachometer/node_modules/koa-mount": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.0.1", - "koa-compose": "^4.1.0" - }, - "engines": { - "node": ">= 7.6.0" - } - }, - "node_modules/tachometer/node_modules/koa-node-resolve": { - "version": "1.0.0-pre.9", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/generator": "^7.4.4", - "@babel/parser": "^7.4.5", - "@babel/traverse": "^7.4.5", - "@types/babel__generator": "^7.6.1", - "@types/parse5": "^5.0.0", - "get-stream": "^5.1.0", - "parse5": "^5.1.0", - "resolve": "^1.11.0" - } - }, - "node_modules/tachometer/node_modules/koa-node-resolve/node_modules/get-stream": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/koa-send": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "resolve-path": "^1.4.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tachometer/node_modules/koa-static": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "koa-send": "^5.0.0" - }, - "engines": { - "node": ">= 7.6.0" - } - }, - "node_modules/tachometer/node_modules/koa-static/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/tachometer/node_modules/lie": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/tachometer/node_modules/lodash.camelcase": { - "version": "4.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.includes": { - "version": "4.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.isboolean": { - "version": "3.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.isinteger": { - "version": "4.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.isnumber": { - "version": "3.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.isplainobject": { - "version": "4.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.isstring": { - "version": "4.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.once": { - "version": "4.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lodash.truncate": { - "version": "4.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/lowercase-keys": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/math-intrinsics": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tachometer/node_modules/media-typer": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/mime-db": { - "version": "1.52.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/mime-types": { - "version": "2.1.35", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/mimic-response": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/negotiator": { - "version": "0.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/nice-try": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/normalize-url": { - "version": "8.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/npm-run-path": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/object-inspect": { - "version": "1.13.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/on-finished": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/tachometer/node_modules/only": { - "version": "0.0.2", - "dev": true - }, - "node_modules/tachometer/node_modules/p-cancelable": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/tachometer/node_modules/p-finally": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/pako": { - "version": "1.0.11", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/tachometer/node_modules/parse5": { - "version": "5.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/parseurl": { - "version": "1.3.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/pkg-install": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/execa": "^0.9.0", - "@types/node": "^11.9.4", - "execa": "^1.0.0" - } - }, - "node_modules/tachometer/node_modules/pkg-install/node_modules/@types/node": { - "version": "11.15.54", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/pkg-up": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^6.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/find-up": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/locate-path": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/path-exists": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/tachometer/node_modules/pkg-up/node_modules/yocto-queue": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/process-nextick-args": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/progress": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/tachometer/node_modules/pump": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/tachometer/node_modules/qs": { - "version": "6.13.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/quick-lru": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/raw-body": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/tachometer/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/reduce-flatten": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tachometer/node_modules/require-from-string": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/resolve": { - "version": "1.22.10", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/resolve-alpn": { - "version": "1.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/resolve-path": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "http-errors": "~1.6.2", - "path-is-absolute": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/tachometer/node_modules/resolve-path/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/resolve-path/node_modules/http-errors": { - "version": "1.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/resolve-path/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/resolve-path/node_modules/setprototypeof": { - "version": "1.1.0", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/responselike": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tachometer/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/tachometer/node_modules/safe-regex-test": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/sanitize-filename": { - "version": "1.6.3", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/tachometer/node_modules/selenium-webdriver": { - "version": "4.27.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/SeleniumHQ" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/selenium" - } - ], - "license": "Apache-2.0", - "dependencies": { - "@bazel/runfiles": "^6.3.1", - "jszip": "^3.10.1", - "tmp": "^0.2.3", - "ws": "^8.18.0" - }, - "engines": { - "node": ">= 14.21.0" - } - }, - "node_modules/tachometer/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tachometer/node_modules/setimmediate": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/setprototypeof": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/side-channel": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/side-channel-list": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/side-channel-map": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/slice-ansi": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/tachometer/node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/tachometer/node_modules/statuses": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tachometer/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/tachometer/node_modules/strip-eof": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tachometer/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tachometer/node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tachometer/node_modules/systeminformation": { - "version": "5.25.10", - "dev": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, - "node_modules/tachometer/node_modules/table": { - "version": "6.9.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/tachometer/node_modules/table-layout": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tachometer/node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/tachometer/node_modules/table/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tachometer/node_modules/table/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/tmp": { - "version": "0.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tachometer/node_modules/toidentifier": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tachometer/node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/tachometer/node_modules/tsscmp": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.x" - } + "node_modules/tachometer/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, - "node_modules/tachometer/node_modules/type-is": { - "version": "1.6.18", + "node_modules/tachometer/node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "dev": true, - "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/tachometer/node_modules/typical": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tachometer/node_modules/ua-parser-js": { - "version": "1.0.40", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" }, "engines": { - "node": "*" + "node": ">=8.0.0" } }, - "node_modules/tachometer/node_modules/undici-types": { - "version": "6.20.0", + "node_modules/tachometer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=0.8.0" + } }, - "node_modules/tachometer/node_modules/universalify": { - "version": "2.0.1", + "node_modules/tachometer/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=12" } }, - "node_modules/tachometer/node_modules/unpipe": { - "version": "1.0.0", + "node_modules/tachometer/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=4" } }, - "node_modules/tachometer/node_modules/utf8-byte-length": { - "version": "1.0.5", + "node_modules/tachometer/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "license": "(WTFPL OR MIT)" + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/tachometer/node_modules/util-deprecate": { + "node_modules/tachometer/node_modules/table-layout": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, - "license": "MIT" + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } }, - "node_modules/tachometer/node_modules/vary": { - "version": "1.1.2", + "node_modules/tachometer/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, "node_modules/tachometer/node_modules/wordwrapjs": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "dev": true, - "license": "MIT", "dependencies": { "reduce-flatten": "^2.0.0", "typical": "^5.2.0" @@ -6926,56 +6467,25 @@ "node": ">=8.0.0" } }, - "node_modules/tachometer/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/tachometer/node_modules/ws": { - "version": "8.18.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/tachometer/node_modules/ylru": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/tar-fs": { - "version": "3.0.6", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", "dev": true, - "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, - "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -7017,21 +6527,33 @@ }, "node_modules/text-decoder": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, "node_modules/through": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=14.14" + } }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7041,16 +6563,18 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/tr46": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, - "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, @@ -7058,31 +6582,44 @@ "node": ">=18" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tslib": { "version": "2.8.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true }, "node_modules/tsscmp": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6.x" } }, "node_modules/type-detect": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -7092,8 +6629,9 @@ }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -7104,13 +6642,15 @@ }, "node_modules/typed-query-selector": { "version": "2.12.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true }, "node_modules/typescript": { - "version": "5.7.2", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7121,16 +6661,44 @@ }, "node_modules/typical": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/uglify-js": { "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, - "license": "BSD-2-Clause", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -7140,8 +6708,9 @@ }, "node_modules/unbzip2-stream": { "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -7149,29 +6718,45 @@ }, "node_modules/undici-types": { "version": "6.20.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, - "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -7183,24 +6768,27 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/webidl-conversions": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", "dev": true, - "license": "MIT", "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" @@ -7211,8 +6799,9 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7225,8 +6814,9 @@ }, "node_modules/wordwrapjs": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.17" } @@ -7239,8 +6829,9 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7268,15 +6859,99 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/ws": { "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -7295,35 +6970,38 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.2", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^4.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yargs-parser": { - "version": "21.1.1", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, - "license": "ISC", "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yargs-unparser": { @@ -7341,10 +7019,52 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yauzl": { "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, - "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -7352,16 +7072,30 @@ }, "node_modules/ylru": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tsconfig.json b/tsconfig.json index b148c83..07da072 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ From e50aec451fc0b2265254b641429bad62fdcca138 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Thu, 16 Jan 2025 09:39:25 -0600 Subject: [PATCH 45/50] for some reason this can hang when assertion fails. --- test/lib/utilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/utilities.js b/test/lib/utilities.js index 908a1be..7dabe7a 100644 --- a/test/lib/utilities.js +++ b/test/lib/utilities.js @@ -87,6 +87,6 @@ function assertFocus(elementId) { } function assertNoFocus() { - document.activeElement.should.equal(document.body); + document.activeElement.tagName.should.eql("BODY"); } From 71c196ea8152f7f95308ca71ec32801f57bb5645 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Thu, 16 Jan 2025 13:54:38 -0600 Subject: [PATCH 46/50] normalize more function argument order and improve docs and tests. --- src/idiomorph.js | 85 ++++++----- test/index.html | 3 +- test/lib/utilities.js | 35 +++++ test/ops.js | 170 +++++++++++++++++++++ test/preserve-focus.js | 340 +++++++++++++++++++++-------------------- 5 files changed, 431 insertions(+), 202 deletions(-) create mode 100644 test/ops.js diff --git a/src/idiomorph.js b/src/idiomorph.js index 91f3f40..8d9d53e 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -254,15 +254,18 @@ var Idiomorph = (function () { * * Basic algorithm: * - for each node in the new content: - * - if there could be a match among self and upcoming siblings - * - search self and siblings for an id set match, falling back to a soft match - * - if found - * - remove any nodes inbetween (pantrying any persistent nodes) - * - morph it and move on - * - if no match and node is persistent - * - move it and all its children here (looking in pantry too) - * - morph it and move on - * - create a new node from scratch as a last result + * - search self and siblings for an id set match, falling back to a soft match + * - if match found + * - remove any nodes up to the match: + * - pantry persistent nodes + * - delete the rest + * - morph the match + * - elsif no match found, and node is persistent + * - find its match by querying the old root (future) and pantry (past) + * - move it and its children here + * - morph it + * - else + * - create a new node from scratch as a last result * * @param {MorphContext} ctx the merge context * @param {Element} oldParent the old content that we are merging the new content into @@ -294,15 +297,15 @@ var Idiomorph = (function () { // once we reach the end of the old parent content skip to the end and insert the rest if (insertionPoint && insertionPoint != endPoint) { const bestMatch = findBestMatch( + ctx, newChild, insertionPoint, endPoint, - ctx, ); if (bestMatch) { - // if the node to morph is not at the insertion point then delete up to it + // if the node to morph is not at the insertion point then remove/move up to it if (bestMatch !== insertionPoint) { - removeNodesBetween(insertionPoint, bestMatch, ctx); + removeNodesBetween(ctx, insertionPoint, bestMatch); } morphNode(bestMatch, newChild, ctx); insertionPoint = bestMatch.nextSibling; @@ -331,7 +334,7 @@ var Idiomorph = (function () { while (insertionPoint && insertionPoint != endPoint) { const tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; - removeNode(tempNode, ctx); + removeNode(ctx, tempNode); } } @@ -368,31 +371,36 @@ var Idiomorph = (function () { //============================================================================= const findBestMatch = (function () { /** - * Scans forward from the insertionPoint to the endPoint looking for a match - * for the newChild. It looks for an id set match first, then a soft match. - * @param {Node} newChild - * @param {Node | null} insertionPoint - * @param {Node | null} endPoint + * Scans forward from the startPoint to the endPoint looking for a match + * for the node. It looks for an id set match first, then a soft match. + * @param {Node} node * @param {MorphContext} ctx + * @param {Node | null} startPoint + * @param {Node | null} endPoint * @returns {Node | null} */ - function findBestMatch(newChild, insertionPoint, endPoint, ctx) { + function findBestMatch(ctx, node, startPoint, endPoint) { let softMatch = null; - let cursor = insertionPoint; + let cursor = startPoint; while (cursor && cursor != endPoint) { // soft matching is a prerequisite for id set matching - if (isSoftMatch(cursor, newChild)) { - if (getIdIntersectionCount(cursor, newChild, ctx) > 0) { + if (isSoftMatch(cursor, node)) { + if (getIdIntersectionCount(cursor, node, ctx) > 0) { return cursor; // found an id set match, we're done! } - // we haven't yet saved the soft match fallback + // we haven't yet saved a soft match fallback if (!softMatch) { // the current soft match will hard match something else in the future, leave it if (!hasPersistentIdNodes(ctx, cursor)) { - // save this as the fallback if we get through the loop without finding a hard match - softMatch = cursor; + // optimization: if node can't id set match, we can just return the soft match immediately + if (!hasPersistentIdNodes(ctx, node)) { + return cursor; + } else { + // save this as the fallback if we get through the loop without finding a hard match + softMatch = cursor; + } } } } @@ -431,15 +439,19 @@ var Idiomorph = (function () { //============================================================================= /** - * - * @param {Node} node + * Gets rid of an unwanted DOM node; strategy depends on nature of its reuse: + * - Persistent nodes will be moved to the pantry for later reuse + * - Other nodes will have their hooks called, and then are removed * @param {MorphContext} ctx + * @param {Node} node */ - function removeNode(node, ctx) { - // skip remove callbacks when we're going to be restoring this from the pantry later + function removeNode(ctx, node) { + // are we going to id set match this later? if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { + // skip callbacks and move to pantry moveBefore(ctx.pantry, node, null); } else { + // remove for realsies if (ctx.callbacks.beforeNodeRemoved(node) === false) return; node.parentNode?.removeChild(node); ctx.callbacks.afterNodeRemoved(node); @@ -447,19 +459,20 @@ var Idiomorph = (function () { } /** - * + * Remove nodes between the start and end nodes + * @param {MorphContext} ctx * @param {Node} startInclusive * @param {Node} endExclusive - * @param {MorphContext} ctx - * @returns {Node | null} + * @returns {Node|null} */ - function removeNodesBetween(startInclusive, endExclusive, ctx) { - /** @type {Node | null} */ let cursor = startInclusive; - // remove nodes until it the end point + function removeNodesBetween(ctx, startInclusive, endExclusive) { + /** @type {Node | null} */ + let cursor = startInclusive; + // remove nodes until the endExclusive node while (cursor && cursor !== endExclusive) { let tempNode = /** @type {Node} */ (cursor); cursor = cursor.nextSibling; - removeNode(tempNode, ctx); + removeNode(ctx, tempNode); } return cursor; } diff --git a/test/index.html b/test/index.html index 340c86f..f835d73 100644 --- a/test/index.html +++ b/test/index.html @@ -44,9 +44,10 @@

Mocha Test Suite

- + +
diff --git a/test/lib/utilities.js b/test/lib/utilities.js index 7dabe7a..5352b4e 100644 --- a/test/lib/utilities.js +++ b/test/lib/utilities.js @@ -90,3 +90,38 @@ function assertNoFocus() { document.activeElement.tagName.should.eql("BODY"); } +function assertOps(before, after, expectedOps) { + let ops = []; + let initial = make(before); + let final = make(after); + let finalCopy = document.importNode(final, true); + Idiomorph.morph(initial, final, { + callbacks: { + beforeNodeMorphed: (oldNode, newNode) => { + // Text node morphs are mostly noise + if (oldNode.nodeType === Node.TEXT_NODE) return; + + ops.push([ + "Morphed", + oldNode.outerHTML || oldNode.textContent, + newNode.outerHTML || newNode.textContent, + ]); + }, + beforeNodeRemoved: (node) => { + ops.push(["Removed", node.outerHTML || node.textContent]); + }, + beforeNodeAdded: (node) => { + ops.push(["Added", node.outerHTML || node.textContent]); + }, + }, + }); + if (JSON.stringify(ops) != JSON.stringify(expectedOps)) { + console.log('test expected Operations is:'); + console.log(expectedOps); + console.log('test failing Operations is:'); + console.log(ops); + } + initial.outerHTML.should.equal(finalCopy.outerHTML); + ops.should.eql(expectedOps); +} + diff --git a/test/ops.js b/test/ops.js new file mode 100644 index 0000000..6b62545 --- /dev/null +++ b/test/ops.js @@ -0,0 +1,170 @@ +// This file describes the ideal operations for many common morphs. +// Skipped tests could be viewed as a TODO list for future improvements. + +describe("morphing operations", function () { + setup(); + + it("removing anonymous siblings", function () { + assertOps("
ABC
", "
B
", [ + ["Morphed", "
ABC
", "
B
"], + ["Removed", "A"], + ["Morphed", "B", "B"], + ["Removed", "C"], + ]); + }); + + it("removing IDed siblings", function () { + assertOps( + `
ABC
`, + `
B
`, + [ + [ + "Morphed", + `
ABC
`, + `
B
`, + ], + ["Removed", `A`], + ["Morphed", `B`, `B`], + ["Removed", `C`], + ], + ); + }); + + it.skip("reordering anonymous siblings", function () { + assertOps( + "
ABC
", + "
CBA
", + [ + [ + "Morphed", + "
ABC
", + "
CBA
", + ], + ["Morphed", "C", "C"], + ["Morphed", "B", "B"], + ["Morphed", "A", "A"], + ], + ); + }); + + it("reordering IDed siblings", function () { + assertOps( + `
ABC
`, + `
CBA
`, + [ + [ + "Morphed", + `
ABC
`, + `
CBA
`, + ], + ["Morphed", `C`, `C`], + ["Morphed", `B`, `B`], + ["Morphed", `A`, `A`], + ], + ); + }); + + it.skip("prepending a new softmatchable node onto the beginning", function () { + assertOps( + "
", + "
New
", + [ + [ + "Morphed", + "
", + "
New
", + ], + ["Added", "
New
"], + ["Morphed", "
", "
"], + ["Morphed", "
", "
"], + ], + ); + }); + + it.skip("inserting a new softmatchable node into the middle", function () { + assertOps( + "
", + "
New
", + [ + [ + "Morphed", + "
", + "
New
", + ], + ["Morphed", "
", "
"], + ["Morphed", "
", "
"], + ["Added", "
New
"], + ["Morphed", "
", "
"], + ["Morphed", "
", "
"], + ], + ); + }); + + it("pushing a new softmatchable node onto the end", function () { + assertOps( + "
", + "
New
", + [ + [ + "Morphed", + "
", + "
New
", + ], + ["Morphed", "
", "
"], + ["Morphed", "
", "
"], + ["Added", "
New
"], + ], + ); + }); + + it.skip("removing a softmatchable node from the front", function () { + assertOps( + "
ABC
", + "
BC
", + [ + [ + "Morphed", + "
ABC
", + "
BC
", + ], + ["Removed", "A"], + ["Morphed", "B", "B"], + ["Morphed", "C", "C"], + ], + ); + }); + + it.skip("removing a softmatchable node from the middle", function () { + assertOps( + "
ABC
", + "
AC
", + [ + [ + "Morphed", + "
ABC
", + "
AC
", + ], + ["Morphed", "A", "A"], + ["Removed", "B"], + ["Morphed", "C", "C"], + ], + ); + }); + + it("removing a softmatchable node from the end", function () { + assertOps( + "
ABC
", + "
AB
", + [ + [ + "Morphed", + "
ABC
", + "
AB
", + ], + ["Morphed", "A", "A"], + ["Morphed", "B", "B"], + ["Removed", "C"], + ], + ); + }); +}); diff --git a/test/preserve-focus.js b/test/preserve-focus.js index c81391f..4381f24 100644 --- a/test/preserve-focus.js +++ b/test/preserve-focus.js @@ -1,25 +1,42 @@ describe("Preserves focus where possible", function () { setup(); - it("preserves focus state and outerHTML morphStyle", function () { - const div = make(` -
- - -
- `); - getWorkArea().append(div); - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(div, finalSrc, { morphStyle: "outerHTML" }); + function assertFocusPreservation( + before, + after, + focusId, + selection, + shouldAssertFocusAndSelection = true, + ) { + getWorkArea().innerHTML = before; + setFocusAndSelection(focusId, selection); + Idiomorph.morph(getWorkArea(), after, { + morphStyle: "innerHTML", + }); + getWorkArea().innerHTML.should.equal(after); + // for when we fall short of the ideal + // these should be considered TODOs for future improvement + if (shouldAssertFocusAndSelection) { + assertFocusAndSelection(focusId, selection); + } + } - getWorkArea().innerHTML.should.equal(finalSrc); + it("preserves focus state and outerHTML morphStyle", function () { + assertFocusPreservation( + ` +
+ + +
`, + ` +
+ + +
`, + "focused", + "b", + false, // skip assertion + ); if (hasMoveBefore()) { assertFocus("focused"); // TODO moveBefore loses selection on Chrome 131.0.6778.264 @@ -31,52 +48,40 @@ describe("Preserves focus where possible", function () { }); it("preserves focus state when previous element is replaced", function () { - getWorkArea().innerHTML = ` -
- - -
- `; - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); - assertFocusAndSelection("focused", "b"); + assertFocusPreservation( + ` +
+ + +
`, + ` +
+ + +
`, + "focused", + "b", + ); }); it("preserves focus state when elements are moved to different levels of the DOM", function () { - getWorkArea().append( - make(` -
- -
- -
-
- `), + assertFocusPreservation( + ` +
+ +
+ +
+
`, + ` +
+ + +
`, + "focused", + "b", + false, // skip assertion ); - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); if (hasMoveBefore()) { assertFocus("focused"); // TODO moveBefore loses selection on Chrome 131.0.6778.264 @@ -88,27 +93,23 @@ describe("Preserves focus where possible", function () { }); it("preserves focus state when focused element is moved between anonymous containers", function () { - getWorkArea().innerHTML = ` -
- -
-
- -
- `; - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
- - -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); + assertFocusPreservation( + ` +
+ +
+
+ +
`, + ` +
+ + +
`, + "focused", + "b", + false, // skip assertion + ); if (hasMoveBefore()) { assertFocus("focused"); // TODO moveBefore loses selection on Chrome 131.0.6778.264 @@ -120,35 +121,29 @@ describe("Preserves focus where possible", function () { }); it("preserves focus state when elements are moved between IDed containers", function () { - getWorkArea().append( - make(` -
-
- -
- -
- `), + assertFocusPreservation( + ` +
+
+ +
+ +
`, + ` +
+
+ +
+ +
`, + "focused", + "b", + false, // skip assertion ); - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
-
- -
- -
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); if (hasMoveBefore()) { assertFocus("focused"); // TODO moveBefore loses selection on Chrome 131.0.6778.264 @@ -160,35 +155,29 @@ describe("Preserves focus where possible", function () { }); it("preserves focus state when focus parent is moved down", function () { - getWorkArea().append( - make(` -
-
- -
-
- -
-
- `), + assertFocusPreservation( + ` +
+
+ +
+
+ +
+
`, + ` +
+
+ +
+
+ +
+
`, + "focused", + "b", + false, // skip assertion ); - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
-
- -
-
- -
-
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); - - getWorkArea().innerHTML.should.equal(finalSrc); if (hasMoveBefore()) { assertFocus("focused"); // TODO moveBefore loses selection on Chrome 131.0.6778.264 @@ -200,35 +189,56 @@ describe("Preserves focus where possible", function () { }); it("preserves focus state when focus parent is moved up", function () { - getWorkArea().append( - make(` -
-
- -
-
- -
-
- `), + assertFocusPreservation( + ` +
+
+ +
+
+ +
+
`, + ` +
+
+ +
+
+ +
+
`, + "focused", + "b", ); - setFocusAndSelection("focused", "b"); - - let finalSrc = ` -
-
- -
-
- -
-
- `; - Idiomorph.morph(getWorkArea(), finalSrc, { - morphStyle: "innerHTML", - }); + }); - getWorkArea().innerHTML.should.equal(finalSrc); - assertFocusAndSelection("focused", "b"); + it("preserves focus state when matching anonymous element is inserted", function () { + assertFocusPreservation( + ` +
+
+ +
+
`, + ` +
+
+
+ +
+
`, + "focused", + "b", + false, + ); + if (hasMoveBefore()) { + assertFocus("focused"); + // TODO moveBefore loses selection on Chrome 131.0.6778.264 + // expect will be fixed in future release + // assertFocusAndSelection("focused", "b"); + } else { + assertNoFocus(); + } }); }); From df4a1fadad443e9eb67e140904ec56fb555256a6 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Thu, 16 Jan 2025 14:31:50 -0600 Subject: [PATCH 47/50] these functions aren't pulling their weight anymore. inline. --- src/idiomorph.js | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 8d9d53e..c2ad3b3 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -350,7 +350,7 @@ var Idiomorph = (function () { */ function createNode(oldParent, newChild, insertionPoint, ctx) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null; - if (hasPersistentIdNodes(ctx, newChild) && newChild instanceof Element) { + if (ctx.idMap.has(newChild) && newChild instanceof Element) { // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm const newEmptyChild = document.createElement(newChild.tagName); oldParent.insertBefore(newEmptyChild, insertionPoint); @@ -393,9 +393,9 @@ var Idiomorph = (function () { // we haven't yet saved a soft match fallback if (!softMatch) { // the current soft match will hard match something else in the future, leave it - if (!hasPersistentIdNodes(ctx, cursor)) { + if (!ctx.idMap.has(cursor)) { // optimization: if node can't id set match, we can just return the soft match immediately - if (!hasPersistentIdNodes(ctx, node)) { + if (!ctx.idMap.has(node)) { return cursor; } else { // save this as the fallback if we get through the loop without finding a hard match @@ -447,7 +447,7 @@ var Idiomorph = (function () { */ function removeNode(ctx, node) { // are we going to id set match this later? - if (hasPersistentIdNodes(ctx, node) && node instanceof Element) { + if (ctx.idMap.has(node) && node instanceof Element) { // skip callbacks and move to pantry moveBefore(ctx.pantry, node, null); } else { @@ -527,33 +527,6 @@ var Idiomorph = (function () { // ID Set Functions //============================================================================= - /** - * - * @type {Set} - */ - let EMPTY_SET = new Set(); - - /** - * - * @param {MorphContext} ctx - * @param {Node} node - * @returns {number} - */ - function getPersistentIdNodeCount(ctx, node) { - let idSet = ctx.idMap.get(node) || EMPTY_SET; - return idSet.size; - } - - /** - * - * @param {MorphContext} ctx - * @param {Node} node - * @returns {boolean} - */ - function hasPersistentIdNodes(ctx, node) { - return getPersistentIdNodeCount(ctx, node) > 0; - } - /** * * @param {Node} oldNode From 9a80532833f156e4ddf08ce2a6bfc8871d0ebc1e Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Thu, 16 Jan 2025 14:37:21 -0600 Subject: [PATCH 48/50] move id set matching into findBestMatch module, and simplify. --- src/idiomorph.js | 57 ++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index c2ad3b3..d8ab021 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -386,7 +386,7 @@ var Idiomorph = (function () { while (cursor && cursor != endPoint) { // soft matching is a prerequisite for id set matching if (isSoftMatch(cursor, node)) { - if (getIdIntersectionCount(cursor, node, ctx) > 0) { + if (isIdSetMatch(ctx, cursor, node)) { return cursor; // found an id set match, we're done! } @@ -410,6 +410,31 @@ var Idiomorph = (function () { return softMatch; } + /** + * + * @param {MorphContext} ctx + * @param {Node} oldNode + * @param {Node} newNode + * @returns {boolean} + */ + function isIdSetMatch(ctx, oldNode, newNode) { + let oldSet = ctx.idMap.get(oldNode); + let newSet = ctx.idMap.get(newNode); + + if (!newSet || !oldSet) return false; + + for (const id of oldSet) { + // a potential match is an id in the new and old nodes that + // has not already been merged into the DOM + // But the newNode content we call this on has not been + // merged yet and we don't allow duplicate IDs so it is simple + if (newSet.has(id)) { + return true; + } + } + return false; + } + /** * * @param {Node} oldNode @@ -523,36 +548,6 @@ var Idiomorph = (function () { } } - //============================================================================= - // ID Set Functions - //============================================================================= - - /** - * - * @param {Node} oldNode - * @param {Node} newNode - * @param {MorphContext} ctx - * @returns {number} - */ - function getIdIntersectionCount(oldNode, newNode, ctx) { - let oldSet = ctx.idMap.get(oldNode); - let newSet = ctx.idMap.get(newNode); - - if (!newSet || !oldSet) return 0; - - let matchCount = 0; - for (const id of oldSet) { - // a potential match is an id in the new and old nodes that - // has not already been merged into the DOM - // But the newNode content we call this on has not been - // merged yet and we don't allow duplicate IDs so it is simple - if (newSet.has(id)) { - ++matchCount; - } - } - return matchCount; - } - return morphChildren; })(); From 4ffac5d38fea41ac0989cea9c1e3669772187b8c Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 26 Jan 2025 12:11:24 -0600 Subject: [PATCH 49/50] this is the correct way to set a boolean attribute to true. Co-authored-by: MichaelWest22 --- src/idiomorph.js | 5 +++-- test/core.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index d8ab021..9930ab1 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -737,8 +737,9 @@ var Idiomorph = (function () { } if (newLiveValue) { if (!ignoreUpdate) { - // TODO: do we really want this? tests say so but it feels wrong - oldElement.setAttribute(attributeName, newLiveValue); + // https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML + // this is the correct way to set a boolean attribute to "true" + oldElement.setAttribute(attributeName, ""); } } else { if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) { diff --git a/test/core.js b/test/core.js index 2847d0d..3b15501 100644 --- a/test/core.js +++ b/test/core.js @@ -427,7 +427,7 @@ describe("Core morphing tests", function () { let finalSrc = ''; Idiomorph.morph(initial, finalSrc, { morphStyle: "outerHTML" }); - initial.outerHTML.should.equal(''); + initial.outerHTML.should.equal(''); initial.checked.should.equal(true); document.body.removeChild(parent); }); @@ -474,7 +474,7 @@ describe("Core morphing tests", function () { // is this a problem at all? parent.innerHTML.should.equal(` `); @@ -506,7 +506,7 @@ describe("Core morphing tests", function () { let finalSrc = ` `; Idiomorph.morph(parent, finalSrc, { morphStyle: "innerHTML" }); From 2a09c5a3c1cdf9af5ac764a1d74c6acea31f8097 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sat, 18 Jan 2025 00:43:01 +1300 Subject: [PATCH 50/50] bring back old softMatch behaviour to reduce churn. --- src/idiomorph.js | 34 +++++++++++++++++++++++++--------- test/ops.js | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 9930ab1..bfa9784 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -373,6 +373,7 @@ var Idiomorph = (function () { /** * Scans forward from the startPoint to the endPoint looking for a match * for the node. It looks for an id set match first, then a soft match. + * We abort softmatching if we find two future soft matches, to reduce churn. * @param {Node} node * @param {MorphContext} ctx * @param {Node | null} startPoint @@ -381,6 +382,8 @@ var Idiomorph = (function () { */ function findBestMatch(ctx, node, startPoint, endPoint) { let softMatch = null; + let nextSibling = node.nextSibling; + let siblingSoftMatchCount = 0; let cursor = startPoint; while (cursor && cursor != endPoint) { @@ -391,23 +394,36 @@ var Idiomorph = (function () { } // we haven't yet saved a soft match fallback - if (!softMatch) { + if (softMatch === null) { // the current soft match will hard match something else in the future, leave it if (!ctx.idMap.has(cursor)) { - // optimization: if node can't id set match, we can just return the soft match immediately - if (!ctx.idMap.has(node)) { - return cursor; - } else { - // save this as the fallback if we get through the loop without finding a hard match - softMatch = cursor; - } + // save this as the fallback if we get through the loop without finding a hard match + softMatch = cursor; } } } + if ( + softMatch === null && + nextSibling && + isSoftMatch(cursor, nextSibling) + ) { + // The next new node has a soft match with this node, so + // increment the count of future soft matches + siblingSoftMatchCount++; + nextSibling = nextSibling.nextSibling; + + // If there are two future soft matches, block soft matching for this node to allow + // future siblings to soft match. This is to reduce churn in the DOM when an element + // is prepended. + if (siblingSoftMatchCount >= 2) { + softMatch = undefined; + } + } + cursor = cursor.nextSibling; } - return softMatch; + return softMatch || null; } /** diff --git a/test/ops.js b/test/ops.js index 6b62545..1a1422d 100644 --- a/test/ops.js +++ b/test/ops.js @@ -167,4 +167,25 @@ describe("morphing operations", function () { ], ); }); + + it("show softMatch aborting on two future soft matches", function () { + // when nodes can't be softMatched because they have different types it will scan ahead + // but it aborts the scan ahead if it finds two nodes ahead in both the new and old content + // that softmatch so it can just insert the mis matched node it is on and get to the matching. + assertOps( + "

", + "
Alert

", + [ + [ + "Morphed", + "

", + "
Alert

", + ], + ["Added", "
Alert
"], + ["Morphed", "

", "

"], + ["Morphed", "

", "

"], + ["Morphed", "
", "
"], + ], + ); + }); });