Explorar o código

Yarn 4 Migration (#32253)

Migrates the Overleaf monorepo package manager from npm (v11) to Yarn 4 (v4.9.1) using node-modules linker mode.

GitOrigin-RevId: 50d32ab01955c15e29679eff9e9e9cfb897fab2d
Anna Claire Fields hai 3 meses
pai
achega
0d64a88a46
Modificáronse 100 ficheiros con 3939 adicións e 295 borrados
  1. 831 0
      .yarn/patches/@codemirror-autocomplete-npm-6.18.4.patch
  2. 381 0
      .yarn/patches/@codemirror-search-npm-6.5.8.patch
  3. 44 0
      .yarn/patches/body-parser-npm-1.20.4.patch
  4. 13 0
      .yarn/patches/cypress-multi-reporters-npm-2.0.5.patch
  5. 13 0
      .yarn/patches/forwarded-npm-0.2.0.patch
  6. 13 0
      .yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch
  7. 13 0
      .yarn/patches/multer-npm-2.1.1.patch
  8. 76 0
      .yarn/patches/node-fetch-npm-2.7.0.patch
  9. 13 0
      .yarn/patches/passport-oauth2-npm-1.6.1.patch
  10. 22 0
      .yarn/patches/pdfjs-dist-npm-5.1.91.patch
  11. 1583 0
      .yarn/patches/referer-parser-npm-0.0.3.patch
  12. 30 0
      .yarn/patches/retry-request-npm-7.0.2-a41087680c.patch
  13. 30 0
      .yarn/patches/retry-request-npm-8.0.2-448ad084c8.patch
  14. 13 0
      .yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch
  15. 57 0
      .yarn/patches/send-npm-0.19.0.patch
  16. 57 0
      .yarn/patches/teeny-request-npm-10.1.0.patch
  17. 58 0
      .yarn/patches/teeny-request-npm-9.0.0-4d571e3c55.patch
  18. 81 0
      .yarn/patches/thread-loader-npm-4.0.2-dab5735f54.patch
  19. 21 0
      .yarnrc.yml
  20. 2 1
      dockerfiles/cypress/Dockerfile
  21. 1 0
      libraries/access-token-encryptor/buildscript.txt
  22. 2 2
      libraries/access-token-encryptor/package.json
  23. 1 0
      libraries/fetch-utils/buildscript.txt
  24. 3 3
      libraries/fetch-utils/package.json
  25. 1 0
      libraries/logger/buildscript.txt
  26. 5 5
      libraries/logger/package.json
  27. 1 0
      libraries/metrics/buildscript.txt
  28. 4 4
      libraries/metrics/package.json
  29. 1 0
      libraries/mongo-utils/buildscript.txt
  30. 2 2
      libraries/mongo-utils/package.json
  31. 1 0
      libraries/o-error/buildscript.txt
  32. 3 3
      libraries/o-error/package.json
  33. 1 1
      libraries/object-persistor/README.md
  34. 1 0
      libraries/object-persistor/buildscript.txt
  35. 6 6
      libraries/object-persistor/package.json
  36. 1 0
      libraries/overleaf-editor-core/buildscript.txt
  37. 3 3
      libraries/overleaf-editor-core/package.json
  38. 1 0
      libraries/promise-utils/buildscript.txt
  39. 2 2
      libraries/promise-utils/package.json
  40. 1 0
      libraries/ranges-tracker/buildscript.txt
  41. 2 2
      libraries/ranges-tracker/package.json
  42. 1 0
      libraries/redis-wrapper/buildscript.txt
  43. 7 7
      libraries/redis-wrapper/package.json
  44. 1 0
      libraries/settings/buildscript.txt
  45. 2 2
      libraries/settings/package.json
  46. 1 0
      libraries/stream-utils/buildscript.txt
  47. 2 2
      libraries/stream-utils/package.json
  48. 1 0
      libraries/validation-tools/buildscript.txt
  49. 3 3
      libraries/validation-tools/package.json
  50. 186 26
      package.json
  51. 14 9
      server-ce/Dockerfile
  52. 1 0
      server-ce/Dockerfile-base
  53. 5 5
      server-ce/genScript.js
  54. 1 1
      server-ce/init_scripts/900_run_web_migrations.sh
  55. 1 1
      server-ce/test/Makefile
  56. 1 1
      server-ce/test/docker-compose.yml
  57. 2 2
      server-ce/test/package.json
  58. 5 3
      services/chat/Dockerfile
  59. 12 12
      services/chat/Makefile
  60. 1 0
      services/chat/buildscript.txt
  61. 2 2
      services/chat/docker-compose.ci.yml
  62. 8 2
      services/chat/docker-compose.yml
  63. 3 3
      services/chat/package.json
  64. 5 3
      services/clsi/Dockerfile
  65. 12 12
      services/clsi/Makefile
  66. 2 1
      services/clsi/buildscript.txt
  67. 8 8
      services/clsi/docker-compose.ci.yml
  68. 14 8
      services/clsi/docker-compose.yml
  69. 10 10
      services/clsi/package.json
  70. 26 0
      services/clsi/test/unit/setup.js
  71. 5 3
      services/contacts/Dockerfile
  72. 12 12
      services/contacts/Makefile
  73. 1 0
      services/contacts/buildscript.txt
  74. 2 2
      services/contacts/docker-compose.ci.yml
  75. 8 2
      services/contacts/docker-compose.yml
  76. 7 7
      services/contacts/package.json
  77. 5 3
      services/docstore/Dockerfile
  78. 12 12
      services/docstore/Makefile
  79. 1 0
      services/docstore/buildscript.txt
  80. 2 2
      services/docstore/docker-compose.ci.yml
  81. 8 2
      services/docstore/docker-compose.yml
  82. 11 11
      services/docstore/package.json
  83. 5 3
      services/document-updater/Dockerfile
  84. 12 12
      services/document-updater/Makefile
  85. 1 0
      services/document-updater/buildscript.txt
  86. 2 2
      services/document-updater/docker-compose.ci.yml
  87. 8 2
      services/document-updater/docker-compose.yml
  88. 13 13
      services/document-updater/package.json
  89. 5 3
      services/filestore/Dockerfile
  90. 12 12
      services/filestore/Makefile
  91. 2 1
      services/filestore/buildscript.txt
  92. 4 4
      services/filestore/docker-compose.ci.yml
  93. 10 4
      services/filestore/docker-compose.yml
  94. 7 7
      services/filestore/package.json
  95. 25 0
      services/filestore/test/unit/setup.js
  96. 5 3
      services/history-v1/Dockerfile
  97. 12 12
      services/history-v1/Makefile
  98. 1 0
      services/history-v1/buildscript.txt
  99. 2 2
      services/history-v1/docker-compose.ci.yml
  100. 8 2
      services/history-v1/docker-compose.yml

+ 831 - 0
.yarn/patches/@codemirror-autocomplete-npm-6.18.4.patch

@@ -0,0 +1,831 @@
+diff --git a/dist/index.cjs b/dist/index.cjs
+index 39215ae..b44cb76 100644
+--- a/dist/index.cjs
++++ b/dist/index.cjs
+@@ -187,16 +187,23 @@ Helper function that returns a transaction spec which inserts a
+ completion's text in the main selection range, and any other
+ selection range that has the same text in front of it.
+ */
+-function insertCompletionText(state$1, text, from, to) {
++function insertCompletionText(state$1, text, from, to, extend) {
+     let { main } = state$1.selection, fromOff = from - main.from, toOff = to - main.from;
+     return Object.assign(Object.assign({}, state$1.changeByRange(range => {
+         if (range != main && from != to &&
+             state$1.sliceDoc(range.from + fromOff, range.from + toOff) != state$1.sliceDoc(from, to))
+             return { range };
+-        let lines = state$1.toText(text);
++        let change = {
++            from: range.from + fromOff,
++            to: to == main.from ? range.to : range.from + toOff,
++            insert: text instanceof state.Text ? text : state$1.toText(text),
++        };
++        if (extend) {
++            extend(state$1, change);
++        }
+         return {
+-            changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: lines },
+-            range: state.EditorSelection.cursor(range.from + fromOff + lines.length)
++            changes: change,
++            range: state.EditorSelection.cursor(change.from + change.insert.length)
+         };
+     })), { scrollIntoView: true, userEvent: "input.complete" });
+ }
+@@ -389,7 +396,9 @@ const completionConfig = state.Facet.define({
+             filterStrict: false,
+             compareCompletions: (a, b) => a.label.localeCompare(b.label),
+             interactionDelay: 75,
+-            updateSyncTime: 100
++            updateSyncTime: 100,
++            // overleaf: default to at top which is default CM6 behaviour
++            unfilteredResultsAtEnd: false
+         }, {
+             defaultKeymap: (a, b) => a && b,
+             closeOnBlur: (a, b) => a && b,
+@@ -744,6 +753,7 @@ function score(option) {
+         (option.type ? 1 : 0);
+ }
+ function sortOptions(active, state) {
++    var _a;
+     let options = [];
+     let sections = null;
+     let addOption = (option) => {
+@@ -763,7 +773,8 @@ function sortOptions(active, state) {
+             let getMatch = a.result.getMatch;
+             if (a.result.filter === false) {
+                 for (let option of a.result.options) {
+-                    addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length));
++                    let defaultScore = conf.unfilteredResultsAtEnd ? -1e9 : 1e9;
++                    addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], defaultScore - options.length));
+                 }
+             }
+             else {
+@@ -790,15 +801,42 @@ function sortOptions(active, state) {
+         }
+     }
+     let result = [], prev = null;
++    const priorityIndices = new Map();
+     let compare = conf.compareCompletions;
+     for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
++        // overleaf: Deduplicate results with dedup options
++        //           The goal is to keep only the highest priority option, in the
++        //           highest scoring position.
++        const key = (_a = opt.completion.deduplicate) === null || _a === void 0 ? void 0 : _a.key;
++        if (key) {
++            // Handle merging specifically for deduplicated items item
++            const currentOptionIndex = priorityIndices.get(key);
++            if (currentOptionIndex === undefined) {
++                priorityIndices.set(key, result.length);
++                result.push(opt);
++                prev = opt.completion;
++            }
++            else {
++                if (result[currentOptionIndex].completion.deduplicate.priority < opt.completion.deduplicate.priority) {
++                    result[currentOptionIndex] = opt;
++                    if (currentOptionIndex === result.length - 1) {
++                        prev = opt.completion;
++                    }
++                }
++            }
++            continue;
++        }
++        // overleaf: end
+         let cur = opt.completion;
+-        if (!prev || prev.label != cur.label || prev.detail != cur.detail ||
+-            (prev.type != null && cur.type != null && prev.type != cur.type) ||
+-            prev.apply != cur.apply || prev.boost != cur.boost)
++        if (!prev || prev.label != cur.label)
++            result.push(opt);
++        // overleaf: we're already handling deduplication, so skip extra merges
++        else if (prev.deduplicate)
+             result.push(opt);
+         else if (score(opt.completion) > score(prev))
+             result[result.length - 1] = opt;
++        else if (opt.completion.info)
++            result[result.length - 1] = opt;
+         prev = opt.completion;
+     }
+     return result;
+@@ -817,8 +855,9 @@ class CompletionDialog {
+             : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled);
+     }
+     static build(active, state, id, prev, conf, didSetActive) {
+-        if (prev && !didSetActive && active.some(s => s.isPending))
+-            return prev.setDisabled();
++        // Overleaf: avoid setting the previous completion state to disabled while completion sources are pending
++        // if (prev && !didSetActive && active.some(s => s.isPending))
++        //   return prev.setDisabled()
+         let options = sortOptions(active, state);
+         if (!options.length)
+             return prev && active.some(a => a.isPending) ? prev.setDisabled() : null;
+@@ -1017,13 +1056,14 @@ const completionState = state.StateField.define({
+         view.EditorView.contentAttributes.from(f, state => state.attrs)
+     ]
+ });
++const getCompletionTooltip = (state) => { var _a; return (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.tooltip; };
+ function applyCompletion(view, option) {
+     const apply = option.completion.apply || option.completion.label;
+     let result = view.state.field(completionState).active.find(a => a.source == option.source);
+     if (!(result instanceof ActiveResult))
+         return false;
+     if (typeof apply == "string")
+-        view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to)), { annotations: pickedCompletion.of(option.completion) }));
++        view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to, option.completion.extend)), { annotations: pickedCompletion.of(option.completion) }));
+     else
+         apply(view, option.completion, result.from, result.to);
+     return true;
+@@ -1559,20 +1599,42 @@ interpreted as indicating a placeholder.
+ function snippet(template) {
+     let snippet = Snippet.parse(template);
+     return (editor, completion, from, to) => {
+-        let { text, ranges } = snippet.instantiate(editor.state, from);
+-        let { main } = editor.state.selection;
+-        let spec = {
+-            changes: { from, to: to == main.from ? main.to : to, insert: state.Text.of(text) },
+-            scrollIntoView: true,
+-            annotations: completion ? [pickedCompletion.of(completion), state.Transaction.userEvent.of("input.complete")] : undefined
+-        };
++        let { main } = editor.state.selection, fromOff = from - main.from, toOff = to - main.from;
++        let ranges = [];
++        let totalOffset = 0;
++        let spec = Object.assign(Object.assign({}, editor.state.changeByRange(range => {
++            if (range != main && from != to &&
++                editor.state.sliceDoc(range.from + fromOff, range.from + toOff) != editor.state.sliceDoc(from, to))
++                return { range };
++            let { text, ranges: fieldRanges } = snippet.instantiate(editor.state, range.from + fromOff);
++            let change = {
++                from: range.from + fromOff,
++                to: range.from + toOff,
++                insert: state.Text.of(text)
++            };
++            let originalTo = change.to;
++            let offset = change.insert.length + fromOff;
++            if (completion.extend) {
++                completion.extend(editor.state, change);
++                offset += originalTo - change.to;
++            }
++            for (const fieldRange of fieldRanges) {
++                ranges.push(new FieldRange(fieldRange.field, fieldRange.from + totalOffset, fieldRange.to + totalOffset));
++            }
++            totalOffset += offset;
++            return {
++                changes: change,
++                range: state.EditorSelection.cursor(change.from + change.insert.length)
++            };
++        })), { scrollIntoView: true, annotations: completion ? [pickedCompletion.of(completion), state.Transaction.userEvent.of("input.complete")] : undefined, effects: [] });
+         if (ranges.length)
+             spec.selection = fieldSelection(ranges, 0);
+         if (ranges.some(r => r.field > 0)) {
+             let active = new ActiveSnippet(ranges, 0);
+-            let effects = spec.effects = [setActive.of(active)];
+-            if (editor.state.field(snippetState, false) === undefined)
+-                effects.push(state.StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
++            spec.effects.push(setActive.of(active));
++            if (editor.state.field(snippetState, false) === undefined) {
++                spec.effects.push(state.StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
++            }
+         }
+         editor.dispatch(editor.state.update(spec));
+     };
+@@ -1746,7 +1808,8 @@ const completeAnyWord = context => {
+ const defaults = {
+     brackets: ["(", "[", "{", "'", '"'],
+     before: ")]}:;>",
+-    stringPrefixes: []
++    stringPrefixes: [],
++    buildInsert: (state, range, open, close) => open + close,
+ };
+ const closeBracketEffect = state.StateEffect.define({
+     map(value, mapping) {
+@@ -1854,8 +1917,8 @@ function insertBracket(state$1, bracket) {
+     for (let tok of tokens) {
+         let closed = closing(state.codePointAt(tok, 0));
+         if (bracket == tok)
+-            return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok + tok) > -1, conf)
+-                : handleOpen(state$1, tok, closed, conf.before || defaults.before);
++            return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok) > -1, tokens.indexOf(tok + tok + tok) > -1, conf)
++                : handleOpen(state$1, tok, closed, conf.before || defaults.before, conf);
+         if (bracket == closed && closedBracketAt(state$1, state$1.selection.main.from))
+             return handleClose(state$1, tok, closed);
+     }
+@@ -1877,17 +1940,21 @@ function prevChar(doc, pos) {
+     let prev = doc.sliceString(pos - 2, pos);
+     return state.codePointSize(state.codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
+ }
+-function handleOpen(state$1, open, close, closeBefore) {
++function handleOpen(state$1, open, close, closeBefore, config) {
++    let buildInsert = config.buildInsert || defaults.buildInsert;
+     let dont = null, changes = state$1.changeByRange(range => {
++        var _a;
+         if (!range.empty)
+             return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
+                 effects: closeBracketEffect.of(range.to + open.length),
+                 range: state.EditorSelection.range(range.anchor + open.length, range.head + open.length) };
+         let next = nextChar(state$1.doc, range.head);
+-        if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
+-            return { changes: { insert: open + close, from: range.head },
+-                effects: closeBracketEffect.of(range.head + open.length),
++        if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) {
++            const insert = (_a = buildInsert(state$1, range, open, close)) !== null && _a !== void 0 ? _a : open + close;
++            return { changes: { insert, from: range.head },
++                effects: insert === open ? [] : closeBracketEffect.of(range.head + open.length),
+                 range: state.EditorSelection.cursor(range.head + open.length) };
++        }
+         return { range: dont = range };
+     });
+     return dont ? null : state$1.update(changes, {
+@@ -1909,18 +1976,36 @@ function handleClose(state$1, _open, close) {
+ }
+ // Handles cases where the open and close token are the same, and
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
+-function handleSame(state$1, token, allowTriple, config) {
++function handleSame(state$1, token, allowDouble, allowTriple, config) {
+     let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes;
++    let buildInsert = config.buildInsert || defaults.buildInsert;
+     let dont = null, changes = state$1.changeByRange(range => {
++        var _a, _b, _c;
+         if (!range.empty)
+             return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
+                 effects: closeBracketEffect.of(range.to + token.length),
+                 range: state.EditorSelection.range(range.anchor + token.length, range.head + token.length) };
+         let pos = range.head, next = nextChar(state$1.doc, pos), start;
+-        if (next == token) {
++        if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
++            (start = canStartStringAt(state$1, pos - 2 * token.length, stringPrefixes)) > -1 &&
++            nodeStart(state$1, start)) {
++            return { changes: { insert: token + token + token + token, from: pos },
++                effects: closeBracketEffect.of(pos + token.length),
++                range: state.EditorSelection.cursor(pos + token.length) };
++        }
++        else if (allowDouble && state$1.sliceDoc(pos - token.length, pos) == token &&
++            (start = canStartStringAt(state$1, pos - token.length, stringPrefixes)) > -1 &&
++            nodeStart(state$1, start)) {
++            let insert = (_a = buildInsert(state$1, range, token, token)) !== null && _a !== void 0 ? _a : token + token;
++            return { changes: { insert, from: pos },
++                effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
++                range: state.EditorSelection.cursor(pos + token.length) };
++        }
++        else if (next == token) {
+             if (nodeStart(state$1, pos)) {
+-                return { changes: { insert: token + token, from: pos },
+-                    effects: closeBracketEffect.of(pos + token.length),
++                let insert = (_b = buildInsert(state$1, range, token, token)) !== null && _b !== void 0 ? _b : token + token;
++                return { changes: { insert, from: pos },
++                    effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+                     range: state.EditorSelection.cursor(pos + token.length) };
+             }
+             else if (closedBracketAt(state$1, pos)) {
+@@ -1930,18 +2015,13 @@ function handleSame(state$1, token, allowTriple, config) {
+                     range: state.EditorSelection.cursor(pos + content.length) };
+             }
+         }
+-        else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
+-            (start = canStartStringAt(state$1, pos - 2 * token.length, stringPrefixes)) > -1 &&
+-            nodeStart(state$1, start)) {
+-            return { changes: { insert: token + token + token + token, from: pos },
+-                effects: closeBracketEffect.of(pos + token.length),
+-                range: state.EditorSelection.cursor(pos + token.length) };
+-        }
+         else if (state$1.charCategorizer(pos)(next) != state.CharCategory.Word) {
+-            if (canStartStringAt(state$1, pos, stringPrefixes) > -1 && !probablyInString(state$1, pos, token, stringPrefixes))
+-                return { changes: { insert: token + token, from: pos },
+-                    effects: closeBracketEffect.of(pos + token.length),
++            if (canStartStringAt(state$1, pos, stringPrefixes) > -1 && !probablyInString(state$1, pos, token, stringPrefixes)) {
++                const insert = (_c = buildInsert(state$1, range, token, token)) !== null && _c !== void 0 ? _c : token + token;
++                return { changes: { insert, from: pos },
++                    effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+                     range: state.EditorSelection.cursor(pos + token.length) };
++            }
+         }
+         return { range: dont = range };
+     });
+@@ -2086,6 +2166,7 @@ exports.completionKeymap = completionKeymap;
+ exports.completionStatus = completionStatus;
+ exports.currentCompletions = currentCompletions;
+ exports.deleteBracketPair = deleteBracketPair;
++exports.getCompletionTooltip = getCompletionTooltip;
+ exports.hasNextSnippetField = hasNextSnippetField;
+ exports.hasPrevSnippetField = hasPrevSnippetField;
+ exports.ifIn = ifIn;
+@@ -2093,8 +2174,10 @@ exports.ifNotIn = ifNotIn;
+ exports.insertBracket = insertBracket;
+ exports.insertCompletionText = insertCompletionText;
+ exports.moveCompletionSelection = moveCompletionSelection;
++exports.nextChar = nextChar;
+ exports.nextSnippetField = nextSnippetField;
+ exports.pickedCompletion = pickedCompletion;
++exports.prevChar = prevChar;
+ exports.prevSnippetField = prevSnippetField;
+ exports.selectedCompletion = selectedCompletion;
+ exports.selectedCompletionIndex = selectedCompletionIndex;
+diff --git a/dist/index.d.cts b/dist/index.d.cts
+index b57b8f6..fce47ab 100644
+--- a/dist/index.d.cts
++++ b/dist/index.d.cts
+@@ -1,6 +1,6 @@
+ import * as _codemirror_state from '@codemirror/state';
+-import { EditorState, ChangeDesc, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
+-import { EditorView, Rect, KeyBinding, Command } from '@codemirror/view';
++import { EditorState, Text, ChangeDesc, TransactionSpec, StateCommand, Transaction, Facet, SelectionRange, Extension, StateEffect } from '@codemirror/state';
++import { EditorView, Rect, KeyBinding, Tooltip, Command } from '@codemirror/view';
+ import * as _lezer_common from '@lezer/common';
+ 
+ /**
+@@ -73,6 +73,19 @@ interface Completion {
+     a `{name}` object.
+     */
+     section?: string | CompletionSection;
++    /**
++    Can be used to alter the change created when the completion is applied
++    */
++    extend?: ExtendCompletion;
++    /**
++    If multiple sources return the same result, use this field to specifiy a
++    deduplication key as well as a priority. For each unique key, only the
++    completion with the highest priority will be shown.
++    */
++    deduplicate?: {
++        key: string;
++        priority: number;
++    };
+ }
+ /**
+ The type returned from
+@@ -306,12 +319,17 @@ This annotation is added to transactions that are produced by
+ picking a completion.
+ */
+ declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
++type ExtendCompletion = (state: EditorState, change: {
++    from: number;
++    to: number;
++    insert: string | Text;
++}) => void;
+ /**
+ Helper function that returns a transaction spec which inserts a
+ completion's text in the main selection range, and any other
+ selection range that has the same text in front of it.
+ */
+-declare function insertCompletionText(state: EditorState, text: string, from: number, to: number): TransactionSpec;
++declare function insertCompletionText(state: EditorState, text: string | Text, from: number, to: number, extend?: ExtendCompletion): TransactionSpec;
+ 
+ interface CompletionConfig {
+     /**
+@@ -441,6 +459,10 @@ interface CompletionConfig {
+     milliseconds.
+     */
+     updateSyncTime?: number;
++    /**
++    overleaf: Move unfiltered results after the filtered ones
++    */
++    unfilteredResultsAtEnd?: boolean;
+ }
+ 
+ /**
+@@ -514,6 +536,8 @@ applies the snippet.
+ */
+ declare function snippetCompletion(template: string, completion: Completion): Completion;
+ 
++declare const getCompletionTooltip: (state: EditorState) => Tooltip | undefined | null;
++
+ /**
+ Returns a command that moves the completion selection forward or
+ backward by the given amount.
+@@ -562,6 +586,11 @@ interface CloseBracketConfig {
+     these prefixes before the opening quote.
+     */
+     stringPrefixes?: string[];
++    /**
++    An optional callback for overriding the content that's inserted
++    based on surrounding characters
++    */
++    buildInsert?: (state: EditorState, range: SelectionRange, open: string, close: string) => string;
+ }
+ /**
+ Extension to enable bracket-closing behavior. When a closeable
+@@ -593,6 +622,8 @@ to programmatically insert brackets—the
+ take care of running this for user input.)
+ */
+ declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
++declare function nextChar(doc: Text, pos: number): string;
++declare function prevChar(doc: Text, pos: number): string;
+ 
+ /**
+ Returns an extension that enables autocompletion.
+@@ -636,4 +667,5 @@ the currently selected completion.
+ */
+ declare function setSelectedCompletion(index: number): StateEffect<unknown>;
+ 
+-export { type CloseBracketConfig, type Completion, CompletionContext, type CompletionInfo, type CompletionResult, type CompletionSection, type CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
++export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
++export type { CloseBracketConfig, Completion, CompletionInfo, CompletionResult, CompletionSection, CompletionSource };
+diff --git a/dist/index.d.ts b/dist/index.d.ts
+index b57b8f6..fce47ab 100644
+--- a/dist/index.d.ts
++++ b/dist/index.d.ts
+@@ -1,6 +1,6 @@
+ import * as _codemirror_state from '@codemirror/state';
+-import { EditorState, ChangeDesc, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
+-import { EditorView, Rect, KeyBinding, Command } from '@codemirror/view';
++import { EditorState, Text, ChangeDesc, TransactionSpec, StateCommand, Transaction, Facet, SelectionRange, Extension, StateEffect } from '@codemirror/state';
++import { EditorView, Rect, KeyBinding, Tooltip, Command } from '@codemirror/view';
+ import * as _lezer_common from '@lezer/common';
+ 
+ /**
+@@ -73,6 +73,19 @@ interface Completion {
+     a `{name}` object.
+     */
+     section?: string | CompletionSection;
++    /**
++    Can be used to alter the change created when the completion is applied
++    */
++    extend?: ExtendCompletion;
++    /**
++    If multiple sources return the same result, use this field to specifiy a
++    deduplication key as well as a priority. For each unique key, only the
++    completion with the highest priority will be shown.
++    */
++    deduplicate?: {
++        key: string;
++        priority: number;
++    };
+ }
+ /**
+ The type returned from
+@@ -306,12 +319,17 @@ This annotation is added to transactions that are produced by
+ picking a completion.
+ */
+ declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
++type ExtendCompletion = (state: EditorState, change: {
++    from: number;
++    to: number;
++    insert: string | Text;
++}) => void;
+ /**
+ Helper function that returns a transaction spec which inserts a
+ completion's text in the main selection range, and any other
+ selection range that has the same text in front of it.
+ */
+-declare function insertCompletionText(state: EditorState, text: string, from: number, to: number): TransactionSpec;
++declare function insertCompletionText(state: EditorState, text: string | Text, from: number, to: number, extend?: ExtendCompletion): TransactionSpec;
+ 
+ interface CompletionConfig {
+     /**
+@@ -441,6 +459,10 @@ interface CompletionConfig {
+     milliseconds.
+     */
+     updateSyncTime?: number;
++    /**
++    overleaf: Move unfiltered results after the filtered ones
++    */
++    unfilteredResultsAtEnd?: boolean;
+ }
+ 
+ /**
+@@ -514,6 +536,8 @@ applies the snippet.
+ */
+ declare function snippetCompletion(template: string, completion: Completion): Completion;
+ 
++declare const getCompletionTooltip: (state: EditorState) => Tooltip | undefined | null;
++
+ /**
+ Returns a command that moves the completion selection forward or
+ backward by the given amount.
+@@ -562,6 +586,11 @@ interface CloseBracketConfig {
+     these prefixes before the opening quote.
+     */
+     stringPrefixes?: string[];
++    /**
++    An optional callback for overriding the content that's inserted
++    based on surrounding characters
++    */
++    buildInsert?: (state: EditorState, range: SelectionRange, open: string, close: string) => string;
+ }
+ /**
+ Extension to enable bracket-closing behavior. When a closeable
+@@ -593,6 +622,8 @@ to programmatically insert brackets—the
+ take care of running this for user input.)
+ */
+ declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
++declare function nextChar(doc: Text, pos: number): string;
++declare function prevChar(doc: Text, pos: number): string;
+ 
+ /**
+ Returns an extension that enables autocompletion.
+@@ -636,4 +667,5 @@ the currently selected completion.
+ */
+ declare function setSelectedCompletion(index: number): StateEffect<unknown>;
+ 
+-export { type CloseBracketConfig, type Completion, CompletionContext, type CompletionInfo, type CompletionResult, type CompletionSection, type CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
++export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
++export type { CloseBracketConfig, Completion, CompletionInfo, CompletionResult, CompletionSection, CompletionSource };
+diff --git a/dist/index.js b/dist/index.js
+index 4729223..9361a53 100644
+--- a/dist/index.js
++++ b/dist/index.js
+@@ -1,4 +1,4 @@
+-import { Annotation, StateEffect, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Text, Transaction, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
++import { Annotation, StateEffect, Text, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Transaction, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
+ import { Direction, logException, showTooltip, EditorView, ViewPlugin, getTooltip, Decoration, WidgetType, keymap } from '@codemirror/view';
+ import { syntaxTree, indentUnit } from '@codemirror/language';
+ 
+@@ -185,16 +185,23 @@ Helper function that returns a transaction spec which inserts a
+ completion's text in the main selection range, and any other
+ selection range that has the same text in front of it.
+ */
+-function insertCompletionText(state, text, from, to) {
++function insertCompletionText(state, text, from, to, extend) {
+     let { main } = state.selection, fromOff = from - main.from, toOff = to - main.from;
+     return Object.assign(Object.assign({}, state.changeByRange(range => {
+         if (range != main && from != to &&
+             state.sliceDoc(range.from + fromOff, range.from + toOff) != state.sliceDoc(from, to))
+             return { range };
+-        let lines = state.toText(text);
++        let change = {
++            from: range.from + fromOff,
++            to: to == main.from ? range.to : range.from + toOff,
++            insert: text instanceof Text ? text : state.toText(text),
++        };
++        if (extend) {
++            extend(state, change);
++        }
+         return {
+-            changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: lines },
+-            range: EditorSelection.cursor(range.from + fromOff + lines.length)
++            changes: change,
++            range: EditorSelection.cursor(change.from + change.insert.length)
+         };
+     })), { scrollIntoView: true, userEvent: "input.complete" });
+ }
+@@ -387,7 +394,9 @@ const completionConfig = /*@__PURE__*/Facet.define({
+             filterStrict: false,
+             compareCompletions: (a, b) => a.label.localeCompare(b.label),
+             interactionDelay: 75,
+-            updateSyncTime: 100
++            updateSyncTime: 100,
++            // overleaf: default to at top which is default CM6 behaviour
++            unfilteredResultsAtEnd: false
+         }, {
+             defaultKeymap: (a, b) => a && b,
+             closeOnBlur: (a, b) => a && b,
+@@ -742,6 +751,7 @@ function score(option) {
+         (option.type ? 1 : 0);
+ }
+ function sortOptions(active, state) {
++    var _a;
+     let options = [];
+     let sections = null;
+     let addOption = (option) => {
+@@ -761,7 +771,8 @@ function sortOptions(active, state) {
+             let getMatch = a.result.getMatch;
+             if (a.result.filter === false) {
+                 for (let option of a.result.options) {
+-                    addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length));
++                    let defaultScore = conf.unfilteredResultsAtEnd ? -1e9 : 1e9;
++                    addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], defaultScore - options.length));
+                 }
+             }
+             else {
+@@ -788,15 +799,42 @@ function sortOptions(active, state) {
+         }
+     }
+     let result = [], prev = null;
++    const priorityIndices = new Map();
+     let compare = conf.compareCompletions;
+     for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
++        // overleaf: Deduplicate results with dedup options
++        //           The goal is to keep only the highest priority option, in the
++        //           highest scoring position.
++        const key = (_a = opt.completion.deduplicate) === null || _a === void 0 ? void 0 : _a.key;
++        if (key) {
++            // Handle merging specifically for deduplicated items item
++            const currentOptionIndex = priorityIndices.get(key);
++            if (currentOptionIndex === undefined) {
++                priorityIndices.set(key, result.length);
++                result.push(opt);
++                prev = opt.completion;
++            }
++            else {
++                if (result[currentOptionIndex].completion.deduplicate.priority < opt.completion.deduplicate.priority) {
++                    result[currentOptionIndex] = opt;
++                    if (currentOptionIndex === result.length - 1) {
++                        prev = opt.completion;
++                    }
++                }
++            }
++            continue;
++        }
++        // overleaf: end
+         let cur = opt.completion;
+-        if (!prev || prev.label != cur.label || prev.detail != cur.detail ||
+-            (prev.type != null && cur.type != null && prev.type != cur.type) ||
+-            prev.apply != cur.apply || prev.boost != cur.boost)
++        if (!prev || prev.label != cur.label)
++            result.push(opt);
++        // overleaf: we're already handling deduplication, so skip extra merges
++        else if (prev.deduplicate)
+             result.push(opt);
+         else if (score(opt.completion) > score(prev))
+             result[result.length - 1] = opt;
++        else if (opt.completion.info)
++            result[result.length - 1] = opt;
+         prev = opt.completion;
+     }
+     return result;
+@@ -815,8 +853,9 @@ class CompletionDialog {
+             : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled);
+     }
+     static build(active, state, id, prev, conf, didSetActive) {
+-        if (prev && !didSetActive && active.some(s => s.isPending))
+-            return prev.setDisabled();
++        // Overleaf: avoid setting the previous completion state to disabled while completion sources are pending
++        // if (prev && !didSetActive && active.some(s => s.isPending))
++        //   return prev.setDisabled()
+         let options = sortOptions(active, state);
+         if (!options.length)
+             return prev && active.some(a => a.isPending) ? prev.setDisabled() : null;
+@@ -1015,13 +1054,14 @@ const completionState = /*@__PURE__*/StateField.define({
+         EditorView.contentAttributes.from(f, state => state.attrs)
+     ]
+ });
++const getCompletionTooltip = (state) => { var _a; return (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.tooltip; };
+ function applyCompletion(view, option) {
+     const apply = option.completion.apply || option.completion.label;
+     let result = view.state.field(completionState).active.find(a => a.source == option.source);
+     if (!(result instanceof ActiveResult))
+         return false;
+     if (typeof apply == "string")
+-        view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to)), { annotations: pickedCompletion.of(option.completion) }));
++        view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to, option.completion.extend)), { annotations: pickedCompletion.of(option.completion) }));
+     else
+         apply(view, option.completion, result.from, result.to);
+     return true;
+@@ -1557,20 +1597,42 @@ interpreted as indicating a placeholder.
+ function snippet(template) {
+     let snippet = Snippet.parse(template);
+     return (editor, completion, from, to) => {
+-        let { text, ranges } = snippet.instantiate(editor.state, from);
+-        let { main } = editor.state.selection;
+-        let spec = {
+-            changes: { from, to: to == main.from ? main.to : to, insert: Text.of(text) },
+-            scrollIntoView: true,
+-            annotations: completion ? [pickedCompletion.of(completion), Transaction.userEvent.of("input.complete")] : undefined
+-        };
++        let { main } = editor.state.selection, fromOff = from - main.from, toOff = to - main.from;
++        let ranges = [];
++        let totalOffset = 0;
++        let spec = Object.assign(Object.assign({}, editor.state.changeByRange(range => {
++            if (range != main && from != to &&
++                editor.state.sliceDoc(range.from + fromOff, range.from + toOff) != editor.state.sliceDoc(from, to))
++                return { range };
++            let { text, ranges: fieldRanges } = snippet.instantiate(editor.state, range.from + fromOff);
++            let change = {
++                from: range.from + fromOff,
++                to: range.from + toOff,
++                insert: Text.of(text)
++            };
++            let originalTo = change.to;
++            let offset = change.insert.length + fromOff;
++            if (completion.extend) {
++                completion.extend(editor.state, change);
++                offset += originalTo - change.to;
++            }
++            for (const fieldRange of fieldRanges) {
++                ranges.push(new FieldRange(fieldRange.field, fieldRange.from + totalOffset, fieldRange.to + totalOffset));
++            }
++            totalOffset += offset;
++            return {
++                changes: change,
++                range: EditorSelection.cursor(change.from + change.insert.length)
++            };
++        })), { scrollIntoView: true, annotations: completion ? [pickedCompletion.of(completion), Transaction.userEvent.of("input.complete")] : undefined, effects: [] });
+         if (ranges.length)
+             spec.selection = fieldSelection(ranges, 0);
+         if (ranges.some(r => r.field > 0)) {
+             let active = new ActiveSnippet(ranges, 0);
+-            let effects = spec.effects = [setActive.of(active)];
+-            if (editor.state.field(snippetState, false) === undefined)
+-                effects.push(StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
++            spec.effects.push(setActive.of(active));
++            if (editor.state.field(snippetState, false) === undefined) {
++                spec.effects.push(StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
++            }
+         }
+         editor.dispatch(editor.state.update(spec));
+     };
+@@ -1744,7 +1806,8 @@ const completeAnyWord = context => {
+ const defaults = {
+     brackets: ["(", "[", "{", "'", '"'],
+     before: ")]}:;>",
+-    stringPrefixes: []
++    stringPrefixes: [],
++    buildInsert: (state, range, open, close) => open + close,
+ };
+ const closeBracketEffect = /*@__PURE__*/StateEffect.define({
+     map(value, mapping) {
+@@ -1852,8 +1915,8 @@ function insertBracket(state, bracket) {
+     for (let tok of tokens) {
+         let closed = closing(codePointAt(tok, 0));
+         if (bracket == tok)
+-            return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok + tok) > -1, conf)
+-                : handleOpen(state, tok, closed, conf.before || defaults.before);
++            return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok) > -1, tokens.indexOf(tok + tok + tok) > -1, conf)
++                : handleOpen(state, tok, closed, conf.before || defaults.before, conf);
+         if (bracket == closed && closedBracketAt(state, state.selection.main.from))
+             return handleClose(state, tok, closed);
+     }
+@@ -1875,17 +1938,21 @@ function prevChar(doc, pos) {
+     let prev = doc.sliceString(pos - 2, pos);
+     return codePointSize(codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
+ }
+-function handleOpen(state, open, close, closeBefore) {
++function handleOpen(state, open, close, closeBefore, config) {
++    let buildInsert = config.buildInsert || defaults.buildInsert;
+     let dont = null, changes = state.changeByRange(range => {
++        var _a;
+         if (!range.empty)
+             return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
+                 effects: closeBracketEffect.of(range.to + open.length),
+                 range: EditorSelection.range(range.anchor + open.length, range.head + open.length) };
+         let next = nextChar(state.doc, range.head);
+-        if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
+-            return { changes: { insert: open + close, from: range.head },
+-                effects: closeBracketEffect.of(range.head + open.length),
++        if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) {
++            const insert = (_a = buildInsert(state, range, open, close)) !== null && _a !== void 0 ? _a : open + close;
++            return { changes: { insert, from: range.head },
++                effects: insert === open ? [] : closeBracketEffect.of(range.head + open.length),
+                 range: EditorSelection.cursor(range.head + open.length) };
++        }
+         return { range: dont = range };
+     });
+     return dont ? null : state.update(changes, {
+@@ -1907,18 +1974,36 @@ function handleClose(state, _open, close) {
+ }
+ // Handles cases where the open and close token are the same, and
+ // possibly triple quotes (as in `"""abc"""`-style quoting).
+-function handleSame(state, token, allowTriple, config) {
++function handleSame(state, token, allowDouble, allowTriple, config) {
+     let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes;
++    let buildInsert = config.buildInsert || defaults.buildInsert;
+     let dont = null, changes = state.changeByRange(range => {
++        var _a, _b, _c;
+         if (!range.empty)
+             return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
+                 effects: closeBracketEffect.of(range.to + token.length),
+                 range: EditorSelection.range(range.anchor + token.length, range.head + token.length) };
+         let pos = range.head, next = nextChar(state.doc, pos), start;
+-        if (next == token) {
++        if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
++            (start = canStartStringAt(state, pos - 2 * token.length, stringPrefixes)) > -1 &&
++            nodeStart(state, start)) {
++            return { changes: { insert: token + token + token + token, from: pos },
++                effects: closeBracketEffect.of(pos + token.length),
++                range: EditorSelection.cursor(pos + token.length) };
++        }
++        else if (allowDouble && state.sliceDoc(pos - token.length, pos) == token &&
++            (start = canStartStringAt(state, pos - token.length, stringPrefixes)) > -1 &&
++            nodeStart(state, start)) {
++            let insert = (_a = buildInsert(state, range, token, token)) !== null && _a !== void 0 ? _a : token + token;
++            return { changes: { insert, from: pos },
++                effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
++                range: EditorSelection.cursor(pos + token.length) };
++        }
++        else if (next == token) {
+             if (nodeStart(state, pos)) {
+-                return { changes: { insert: token + token, from: pos },
+-                    effects: closeBracketEffect.of(pos + token.length),
++                let insert = (_b = buildInsert(state, range, token, token)) !== null && _b !== void 0 ? _b : token + token;
++                return { changes: { insert, from: pos },
++                    effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+                     range: EditorSelection.cursor(pos + token.length) };
+             }
+             else if (closedBracketAt(state, pos)) {
+@@ -1928,18 +2013,13 @@ function handleSame(state, token, allowTriple, config) {
+                     range: EditorSelection.cursor(pos + content.length) };
+             }
+         }
+-        else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
+-            (start = canStartStringAt(state, pos - 2 * token.length, stringPrefixes)) > -1 &&
+-            nodeStart(state, start)) {
+-            return { changes: { insert: token + token + token + token, from: pos },
+-                effects: closeBracketEffect.of(pos + token.length),
+-                range: EditorSelection.cursor(pos + token.length) };
+-        }
+         else if (state.charCategorizer(pos)(next) != CharCategory.Word) {
+-            if (canStartStringAt(state, pos, stringPrefixes) > -1 && !probablyInString(state, pos, token, stringPrefixes))
+-                return { changes: { insert: token + token, from: pos },
+-                    effects: closeBracketEffect.of(pos + token.length),
++            if (canStartStringAt(state, pos, stringPrefixes) > -1 && !probablyInString(state, pos, token, stringPrefixes)) {
++                const insert = (_c = buildInsert(state, range, token, token)) !== null && _c !== void 0 ? _c : token + token;
++                return { changes: { insert, from: pos },
++                    effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+                     range: EditorSelection.cursor(pos + token.length) };
++            }
+         }
+         return { range: dont = range };
+     });
+@@ -2071,4 +2151,4 @@ function setSelectedCompletion(index) {
+     return setSelectedEffect.of(index);
+ }
+ 
+-export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
++export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };

+ 381 - 0
.yarn/patches/@codemirror-search-npm-6.5.8.patch

@@ -0,0 +1,381 @@
+diff --git a/dist/index.cjs b/dist/index.cjs
+index 46231ae..fb0f9aa 100644
+--- a/dist/index.cjs
++++ b/dist/index.cjs
+@@ -592,6 +592,7 @@ class SearchQuery {
+         this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
+         this.unquoted = this.unquote(this.search);
+         this.wholeWord = !!config.wholeWord;
++        this.scope = config.scope;
+     }
+     /**
+     @internal
+@@ -606,7 +607,7 @@ class SearchQuery {
+     eq(other) {
+         return this.search == other.search && this.replace == other.replace &&
+             this.caseSensitive == other.caseSensitive && this.regexp == other.regexp &&
+-            this.wholeWord == other.wholeWord;
++            this.wholeWord == other.wholeWord && this.scope == other.scope;
+     }
+     /**
+     @internal
+@@ -631,7 +632,12 @@ class QueryType {
+     }
+ }
+ function stringCursor(spec, state, from, to) {
+-    return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined);
++    const test = spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined;
++    const testWithinScope = (from, to, buffer, bufferPos) => {
++        return (!test || test(from, to, buffer, bufferPos))
++            && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
++    };
++    return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), testWithinScope);
+ }
+ function stringWordTest(doc, categorizer) {
+     return (from, to, buf, bufPos) => {
+@@ -695,9 +701,14 @@ class StringQuery extends QueryType {
+     }
+ }
+ function regexpCursor(spec, state, from, to) {
++    const test = spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined;
++    const testWithinScope = (from, to, match) => {
++        return (!test || test(from, to, match))
++            && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
++    };
+     return new RegExpCursor(state.doc, spec.search, {
+         ignoreCase: !spec.caseSensitive,
+-        test: spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
++        test: testWithinScope,
+     }, from, to);
+ }
+ function charBefore(str, index) {
+@@ -737,10 +748,18 @@ class RegExpQuery extends QueryType {
+             this.prevMatchInRange(state, curTo, state.doc.length);
+     }
+     getReplacement(result) {
+-        return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
+-            : i == "&" ? result.match[0]
+-                : i != "0" && +i < result.match.length ? result.match[i]
+-                    : m);
++        return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g, (m, i) => {
++            if (i == "&")
++                return result.match[0];
++            if (i == "$")
++                return "$";
++            for (let l = i.length; l > 0; l--) {
++                let n = +i.slice(0, l);
++                if (n > 0 && n < result.match.length)
++                    return result.match[n] + i.slice(l);
++            }
++            return m;
++        });
+     }
+     matchAll(state, limit) {
+         let cursor = regexpCursor(this.spec, state, 0, state.doc.length), ranges = [];
+@@ -1227,7 +1246,9 @@ const searchExtensions = [
+ exports.RegExpCursor = RegExpCursor;
+ exports.SearchCursor = SearchCursor;
+ exports.SearchQuery = SearchQuery;
++exports.StringQuery = StringQuery;
+ exports.closeSearchPanel = closeSearchPanel;
++exports.createSearchPanel = createSearchPanel;
+ exports.findNext = findNext;
+ exports.findPrevious = findPrevious;
+ exports.getSearchQuery = getSearchQuery;
+@@ -1242,4 +1263,6 @@ exports.searchPanelOpen = searchPanelOpen;
+ exports.selectMatches = selectMatches;
+ exports.selectNextOccurrence = selectNextOccurrence;
+ exports.selectSelectionMatches = selectSelectionMatches;
++exports.selectWord = selectWord;
+ exports.setSearchQuery = setSearchQuery;
++exports.togglePanel = togglePanel;
+diff --git a/dist/index.d.cts b/dist/index.d.cts
+index 08f5696..663d192 100644
+--- a/dist/index.d.cts
++++ b/dist/index.d.cts
+@@ -1,6 +1,6 @@
+ import * as _codemirror_state from '@codemirror/state';
+ import { Text, Extension, StateCommand, EditorState, SelectionRange, StateEffect } from '@codemirror/state';
+-import { Command, KeyBinding, EditorView, Panel } from '@codemirror/view';
++import { Command, EditorView, Panel, KeyBinding } from '@codemirror/view';
+ 
+ /**
+ A search cursor provides an iterator over text matches in a
+@@ -161,6 +161,7 @@ the `"cm-selectionMatch"` class for the highlighting. When
+ itself will be highlighted with `"cm-selectionMatch-main"`.
+ */
+ declare function highlightSelectionMatches(options?: HighlightOptions): Extension;
++declare const selectWord: StateCommand;
+ /**
+ Select next occurrence of the current selection. Expand selection
+ to the surrounding word when the selection is empty.
+@@ -264,6 +265,13 @@ declare class SearchQuery {
+     */
+     readonly wholeWord: boolean;
+     /**
++    When set, only include search matches within these ranges
++    */
++    readonly scope?: Readonly<{
++        from: number;
++        to: number;
++    }[]>;
++    /**
+     Create a query object.
+     */
+     constructor(config: {
+@@ -293,6 +301,13 @@ declare class SearchQuery {
+         Enable whole-word matching.
+         */
+         wholeWord?: boolean;
++        /**
++        The ranges to match within
++        */
++        scope?: Readonly<{
++            from: number;
++            to: number;
++        }[]>;
+     });
+     /**
+     Compare this query to another query.
+@@ -307,6 +322,34 @@ declare class SearchQuery {
+         to: number;
+     }>;
+ }
++type SearchResult = typeof SearchCursor.prototype.value;
++declare abstract class QueryType<Result extends SearchResult = SearchResult> {
++    readonly spec: SearchQuery;
++    constructor(spec: SearchQuery);
++    abstract nextMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
++    abstract prevMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
++    abstract getReplacement(result: Result): string;
++    abstract matchAll(state: EditorState, limit: number): readonly Result[] | null;
++    abstract highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
++}
++declare class StringQuery extends QueryType<SearchResult> {
++    constructor(spec: SearchQuery);
++    nextMatch(state: EditorState, curFrom: number, curTo: number): {
++        from: number;
++        to: number;
++    } | null;
++    private prevMatchInRange;
++    prevMatch(state: EditorState, curFrom: number, curTo: number): {
++        from: number;
++        to: number;
++    } | null;
++    getReplacement(_result: SearchResult): string;
++    matchAll(state: EditorState, limit: number): {
++        from: number;
++        to: number;
++    }[] | null;
++    highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
++}
+ /**
+ A state effect that updates the current search query. Note that
+ this only has an effect if the search state has been initialized
+@@ -315,6 +358,7 @@ by running [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSea
+ once).
+ */
+ declare const setSearchQuery: _codemirror_state.StateEffectType<SearchQuery>;
++declare const togglePanel: _codemirror_state.StateEffectType<boolean>;
+ /**
+ Get the current search query from an editor state.
+ */
+@@ -353,6 +397,7 @@ Replace all instances of the search query with the given
+ replacement.
+ */
+ declare const replaceAll: Command;
++declare function createSearchPanel(view: EditorView): Panel;
+ /**
+ Make sure the search panel is open and focused.
+ */
+@@ -372,4 +417,4 @@ Default search-related key bindings.
+ */
+ declare const searchKeymap: readonly KeyBinding[];
+ 
+-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
++export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };
+diff --git a/dist/index.d.ts b/dist/index.d.ts
+index 08f5696..663d192 100644
+--- a/dist/index.d.ts
++++ b/dist/index.d.ts
+@@ -1,6 +1,6 @@
+ import * as _codemirror_state from '@codemirror/state';
+ import { Text, Extension, StateCommand, EditorState, SelectionRange, StateEffect } from '@codemirror/state';
+-import { Command, KeyBinding, EditorView, Panel } from '@codemirror/view';
++import { Command, EditorView, Panel, KeyBinding } from '@codemirror/view';
+ 
+ /**
+ A search cursor provides an iterator over text matches in a
+@@ -161,6 +161,7 @@ the `"cm-selectionMatch"` class for the highlighting. When
+ itself will be highlighted with `"cm-selectionMatch-main"`.
+ */
+ declare function highlightSelectionMatches(options?: HighlightOptions): Extension;
++declare const selectWord: StateCommand;
+ /**
+ Select next occurrence of the current selection. Expand selection
+ to the surrounding word when the selection is empty.
+@@ -264,6 +265,13 @@ declare class SearchQuery {
+     */
+     readonly wholeWord: boolean;
+     /**
++    When set, only include search matches within these ranges
++    */
++    readonly scope?: Readonly<{
++        from: number;
++        to: number;
++    }[]>;
++    /**
+     Create a query object.
+     */
+     constructor(config: {
+@@ -293,6 +301,13 @@ declare class SearchQuery {
+         Enable whole-word matching.
+         */
+         wholeWord?: boolean;
++        /**
++        The ranges to match within
++        */
++        scope?: Readonly<{
++            from: number;
++            to: number;
++        }[]>;
+     });
+     /**
+     Compare this query to another query.
+@@ -307,6 +322,34 @@ declare class SearchQuery {
+         to: number;
+     }>;
+ }
++type SearchResult = typeof SearchCursor.prototype.value;
++declare abstract class QueryType<Result extends SearchResult = SearchResult> {
++    readonly spec: SearchQuery;
++    constructor(spec: SearchQuery);
++    abstract nextMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
++    abstract prevMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
++    abstract getReplacement(result: Result): string;
++    abstract matchAll(state: EditorState, limit: number): readonly Result[] | null;
++    abstract highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
++}
++declare class StringQuery extends QueryType<SearchResult> {
++    constructor(spec: SearchQuery);
++    nextMatch(state: EditorState, curFrom: number, curTo: number): {
++        from: number;
++        to: number;
++    } | null;
++    private prevMatchInRange;
++    prevMatch(state: EditorState, curFrom: number, curTo: number): {
++        from: number;
++        to: number;
++    } | null;
++    getReplacement(_result: SearchResult): string;
++    matchAll(state: EditorState, limit: number): {
++        from: number;
++        to: number;
++    }[] | null;
++    highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
++}
+ /**
+ A state effect that updates the current search query. Note that
+ this only has an effect if the search state has been initialized
+@@ -315,6 +358,7 @@ by running [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSea
+ once).
+ */
+ declare const setSearchQuery: _codemirror_state.StateEffectType<SearchQuery>;
++declare const togglePanel: _codemirror_state.StateEffectType<boolean>;
+ /**
+ Get the current search query from an editor state.
+ */
+@@ -353,6 +397,7 @@ Replace all instances of the search query with the given
+ replacement.
+ */
+ declare const replaceAll: Command;
++declare function createSearchPanel(view: EditorView): Panel;
+ /**
+ Make sure the search panel is open and focused.
+ */
+@@ -372,4 +417,4 @@ Default search-related key bindings.
+ */
+ declare const searchKeymap: readonly KeyBinding[];
+ 
+-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
++export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };
+diff --git a/dist/index.js b/dist/index.js
+index 22172ef..08a9974 100644
+--- a/dist/index.js
++++ b/dist/index.js
+@@ -590,6 +590,7 @@ class SearchQuery {
+         this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
+         this.unquoted = this.unquote(this.search);
+         this.wholeWord = !!config.wholeWord;
++        this.scope = config.scope;
+     }
+     /**
+     @internal
+@@ -604,7 +605,7 @@ class SearchQuery {
+     eq(other) {
+         return this.search == other.search && this.replace == other.replace &&
+             this.caseSensitive == other.caseSensitive && this.regexp == other.regexp &&
+-            this.wholeWord == other.wholeWord;
++            this.wholeWord == other.wholeWord && this.scope == other.scope;
+     }
+     /**
+     @internal
+@@ -629,7 +630,12 @@ class QueryType {
+     }
+ }
+ function stringCursor(spec, state, from, to) {
+-    return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined);
++    const test = spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined;
++    const testWithinScope = (from, to, buffer, bufferPos) => {
++        return (!test || test(from, to, buffer, bufferPos))
++            && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
++    };
++    return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), testWithinScope);
+ }
+ function stringWordTest(doc, categorizer) {
+     return (from, to, buf, bufPos) => {
+@@ -693,9 +699,14 @@ class StringQuery extends QueryType {
+     }
+ }
+ function regexpCursor(spec, state, from, to) {
++    const test = spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined;
++    const testWithinScope = (from, to, match) => {
++        return (!test || test(from, to, match))
++            && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
++    };
+     return new RegExpCursor(state.doc, spec.search, {
+         ignoreCase: !spec.caseSensitive,
+-        test: spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
++        test: testWithinScope,
+     }, from, to);
+ }
+ function charBefore(str, index) {
+@@ -735,10 +746,18 @@ class RegExpQuery extends QueryType {
+             this.prevMatchInRange(state, curTo, state.doc.length);
+     }
+     getReplacement(result) {
+-        return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
+-            : i == "&" ? result.match[0]
+-                : i != "0" && +i < result.match.length ? result.match[i]
+-                    : m);
++        return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g, (m, i) => {
++            if (i == "&")
++                return result.match[0];
++            if (i == "$")
++                return "$";
++            for (let l = i.length; l > 0; l--) {
++                let n = +i.slice(0, l);
++                if (n > 0 && n < result.match.length)
++                    return result.match[n] + i.slice(l);
++            }
++            return m;
++        });
+     }
+     matchAll(state, limit) {
+         let cursor = regexpCursor(this.spec, state, 0, state.doc.length), ranges = [];
+@@ -1222,4 +1241,4 @@ const searchExtensions = [
+     baseTheme
+ ];
+ 
+-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
++export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };

+ 44 - 0
.yarn/patches/body-parser-npm-1.20.4.patch

@@ -0,0 +1,44 @@
+diff --git a/lib/read.js b/lib/read.js
+index fce6283..6131c31 100644
+--- a/lib/read.js
++++ b/lib/read.js
+@@ -18,7 +18,7 @@ var iconv = require('iconv-lite')
+ var onFinished = require('on-finished')
+ var unpipe = require('unpipe')
+ var zlib = require('zlib')
+-
++var Stream = require('stream')
+ /**
+  * Module exports.
+  */
+@@ -166,25 +166,25 @@ function contentstream (req, debug, inflate) {
+     case 'deflate':
+       stream = zlib.createInflate()
+       debug('inflate body')
+-      req.pipe(stream)
++      // req.pipe(stream)
+       break
+     case 'gzip':
+       stream = zlib.createGunzip()
+       debug('gunzip body')
+-      req.pipe(stream)
++      // req.pipe(stream)
+       break
+     case 'identity':
+       stream = req
+       stream.length = length
+-      break
++      return req
+     default:
+       throw createError(415, 'unsupported content encoding "' + encoding + '"', {
+         encoding: encoding,
+         type: 'encoding.unsupported'
+       })
+   }
+-
+-  return stream
++  var pass = new Stream.PassThrough(); Stream.pipeline(req, stream, pass, () => {})
++  return pass
+ }
+ 
+ /**

+ 13 - 0
.yarn/patches/cypress-multi-reporters-npm-2.0.5.patch

@@ -0,0 +1,13 @@
+diff --git a/lib/MultiReporters.js b/lib/MultiReporters.js
+index 98dc4ef..b2a97bf 100644
+--- a/lib/MultiReporters.js
++++ b/lib/MultiReporters.js
+@@ -160,7 +160,7 @@ MultiReporters.prototype.getCustomOptions = function (options) {
+         debug('options file (custom)', customOptionsFile);
+ 
+         try {
+-            if ('.js' === path.extname(customOptionsFile)) {
++            if (['.js', '.cjs'].includes(path.extname(customOptionsFile))) {
+                 customOptions = require(customOptionsFile);
+             }
+             else {

+ 13 - 0
.yarn/patches/forwarded-npm-0.2.0.patch

@@ -0,0 +1,13 @@
+diff --git a/index.js b/index.js
+index b2b6bdd..75e6254 100644
+--- a/index.js
++++ b/index.js
+@@ -46,7 +46,7 @@ function forwarded (req) {
+ function getSocketAddr (req) {
+   return req.socket
+     ? req.socket.remoteAddress
+-    : req.connection.remoteAddress
++    : req.connection && req.connection.remoteAddress
+ }
+ 
+ /**

+ 13 - 0
.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch

@@ -0,0 +1,13 @@
+diff --git a/lib/MultiReporters.js b/lib/MultiReporters.js
+index d61e019711d5ac7f82c0fb90548bb0eb41ebbb85..e7a9515e05287621301b831191884a84d72ad0a1 100644
+--- a/lib/MultiReporters.js
++++ b/lib/MultiReporters.js
+@@ -153,7 +153,7 @@ MultiReporters.prototype.getCustomOptions = function (options) {
+         debug('options file (custom)', customOptionsFile);
+ 
+         try {
+-            if ('.js' === path.extname(customOptionsFile)) {
++            if (['.js', '.cjs'].includes(path.extname(customOptionsFile))) {
+                 customOptions = require(customOptionsFile);
+             }
+             else {

+ 13 - 0
.yarn/patches/multer-npm-2.1.1.patch

@@ -0,0 +1,13 @@
+diff --git a/lib/make-middleware.js b/lib/make-middleware.js
+index ee50988..de77364 100644
+--- a/lib/make-middleware.js
++++ b/lib/make-middleware.js
+@@ -164,7 +164,7 @@ function makeMiddleware (setup) {
+       if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
+ 
+       // don't attach to the files object, if there is no file
+-      if (!filename) return fileStream.resume()
++      if (!filename) filename = 'undefined'
+ 
+       // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
+       if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {

+ 76 - 0
.yarn/patches/node-fetch-npm-2.7.0.patch

@@ -0,0 +1,76 @@
+diff --git a/lib/index.js b/lib/index.js
+index 567ff5d..8eb45f7 100644
+--- a/lib/index.js
++++ b/lib/index.js
+@@ -545,8 +545,8 @@ function clone(instance) {
+ 		// tee instance body
+ 		p1 = new PassThrough();
+ 		p2 = new PassThrough();
+-		body.pipe(p1);
+-		body.pipe(p2);
++		Stream.pipeline(body, p1, () => {});
++		Stream.pipeline(body, p2, () => {});
+ 		// set instance body to teed body and return the other teed body
+ 		instance[INTERNALS].body = p1;
+ 		body = p2;
+@@ -648,14 +648,14 @@ function writeToStream(dest, instance) {
+ 		// body is null
+ 		dest.end();
+ 	} else if (isBlob(body)) {
+-		body.stream().pipe(dest);
++		Stream.pipeline(body.stream(), dest, () => {});
+ 	} else if (Buffer.isBuffer(body)) {
+ 		// body is buffer
+ 		dest.write(body);
+ 		dest.end();
+ 	} else {
+ 		// body is stream
+-		body.pipe(dest);
++		Stream.pipeline(body, dest, () => {});
+ 	}
+ }
+ 
+@@ -1638,7 +1638,7 @@ function fetch(url, opts) {
+ 			res.once('end', function () {
+ 				if (signal) signal.removeEventListener('abort', abortAndFinalize);
+ 			});
+-			let body = res.pipe(new PassThrough$1());
++			let body = Stream.pipeline(res, new PassThrough(), error => { if (error) reject(error); });
+ 
+ 			const response_options = {
+ 				url: request.url,
+@@ -1679,7 +1679,7 @@ function fetch(url, opts) {
+ 
+ 			// for gzip
+ 			if (codings == 'gzip' || codings == 'x-gzip') {
+-				body = body.pipe(zlib.createGunzip(zlibOptions));
++				body = Stream.pipeline(body, zlib.createGunzip(zlibOptions), error => { if (error) reject(error); });
+ 				response = new Response(body, response_options);
+ 				resolve(response);
+ 				return;
+@@ -1689,13 +1689,13 @@ function fetch(url, opts) {
+ 			if (codings == 'deflate' || codings == 'x-deflate') {
+ 				// handle the infamous raw deflate response from old servers
+ 				// a hack for old IIS and Apache servers
+-				const raw = res.pipe(new PassThrough$1());
++				const raw = Stream.pipeline(res, new PassThrough(), error => { if (error) reject(error); });
+ 				raw.once('data', function (chunk) {
+ 					// see http://stackoverflow.com/questions/37519828
+ 					if ((chunk[0] & 0x0F) === 0x08) {
+-						body = body.pipe(zlib.createInflate());
++						body = Stream.pipeline(body, zlib.createInflate(), error => { if (error) reject(error); });
+ 					} else {
+-						body = body.pipe(zlib.createInflateRaw());
++						body = Stream.pipeline(body, zlib.createInflateRaw(), error => { if (error) reject(error); });
+ 					}
+ 					response = new Response(body, response_options);
+ 					resolve(response);
+@@ -1712,7 +1712,7 @@ function fetch(url, opts) {
+ 
+ 			// for br
+ 			if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') {
+-				body = body.pipe(zlib.createBrotliDecompress());
++				body = Stream.pipeline(body, zlib.createBrotliDecompress(), error => { if (error) reject(error); });
+ 				response = new Response(body, response_options);
+ 				resolve(response);
+ 				return;

+ 13 - 0
.yarn/patches/passport-oauth2-npm-1.6.1.patch

@@ -0,0 +1,13 @@
+diff --git a/lib/utils.js b/lib/utils.js
+index 486f9e1..4584507 100644
+--- a/lib/utils.js
++++ b/lib/utils.js
+@@ -24,7 +24,7 @@ exports.originalURL = function(req, options) {
+   var trustProxy = options.proxy;
+   
+   var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase()
+-    , tls = req.connection.encrypted || (trustProxy && 'https' == proto.split(/\s*,\s*/)[0])
++    , tls = (req.connection && req.connection.encrypted) || (trustProxy && 'https' == proto.split(/\s*,\s*/)[0])
+     , host = (trustProxy && req.headers['x-forwarded-host']) || req.headers.host
+     , protocol = tls ? 'https' : 'http'
+     , path = req.url || '';

+ 22 - 0
.yarn/patches/pdfjs-dist-npm-5.1.91.patch

@@ -0,0 +1,22 @@
+diff --git a/build/pdf.worker.mjs b/build/pdf.worker.mjs
+index 6c5c6f1..bb6b7d1 100644
+--- a/build/pdf.worker.mjs
++++ b/build/pdf.worker.mjs
+@@ -1830,7 +1830,7 @@ async function __wbg_init(module_or_path) {
+     }
+   }
+   if (typeof module_or_path === 'undefined') {
+-    module_or_path = new URL('qcms_bg.wasm', import.meta.url);
++    module_or_path = new URL(/* webpackIgnore: true */ 'qcms_bg.wasm', import.meta.url);
+   }
+   const imports = __wbg_get_imports();
+   if (typeof module_or_path === 'string' || typeof Request === 'function' && module_or_path instanceof Request || typeof URL === 'function' && module_or_path instanceof URL) {
+@@ -5358,7 +5358,7 @@ var OpenJPEG = (() => {
+       if (Module["locateFile"]) {
+         return locateFile("openjpeg.wasm");
+       }
+-      return new URL("openjpeg.wasm", import.meta.url).href;
++      return new URL(/* webpackIgnore: true */ "openjpeg.wasm", import.meta.url).href;
+     }
+     function getBinarySync(file) {
+       if (file == wasmBinaryFile && wasmBinary) {

+ 1583 - 0
.yarn/patches/referer-parser-npm-0.0.3.patch

@@ -0,0 +1,1583 @@
+diff --git a/package.json b/package.json
+--- a/package.json
++++ b/package.json
+@@ -1,6 +1,6 @@
+ {
+   "name": "referer-parser",
+-  "version": "0.0.3",
++  "version": "0.0.4",
+   "description": "library for extracting marketing attribution data (such as search terms) from referrer URLs",
+   "main": "index.js",
+   "scripts": {
+@@ -15,9 +15,9 @@
+     }
+   ],
+   "dependencies": {
+-    "js-yaml": "~2.1.0"
++    "js-yaml": "^4.1.0"
+   },
+   "devDependencies": {
+-    "mocha": "~1.12.0"
++    "mocha": "~10.2.0"
+   }
+ }
+diff --git a/index.js b/index.js
+--- a/index.js
++++ b/index.js
+@@ -123,11 +123,10 @@
+           ref_path, include_path
+       )
+     } catch (e) {
+-      console.error(e)
+       return null
+     }
+   } else return referer
+ };
+ 
+ // var r = new Referer("http://www.google.com/search?q=gateway+oracle+cards+denise+linn&hl=en&client=safari")
+-// console.log(r.uri)
+\ No newline at end of file
++// console.log(r.uri)
+diff --git a/data/referers.yml b/data/referers.yml
+--- a/data/referers.yml
++++ b/data/referers.yml
+@@ -1,9 +1,3 @@
+-#################################### DO NOT EDIT THIS FILE ####################################
+-#                                                                                             #  
+-#             This file will be overwritten the next time ./sync_yaml.sh is run!              #
+-#                                                                                             #
+-#################################### DO NOT EDIT THIS FILE ####################################
+-
+ # #######################################################################################################
+ #
+ # ALL SUPPORTED REFERERS
+@@ -14,6 +8,7 @@
+ # 2. Email providers
+ # 3. Social providers
+ # 4. Search providers
++# 5. Paid media
+ 
+ 
+ # #######################################################################################################
+@@ -35,7 +30,6 @@
+       - sites.google.com
+       - groups.google.com
+       - groups.google.co.uk
+-      - news.google.co.uk
+ 
+   Yahoo!:
+     domains:
+@@ -57,6 +51,15 @@
+       - omg.yahoo.com
+       - match.yahoo.net
+ 
++  Yandex Maps:
++    parameters:
++      - text
++    domains:
++      - maps.yandex.ru
++      - maps.yandex.ua
++      - maps.yandex.com
++      - maps.yandex.by
++      - n.maps.yandex.ru
+ 
+ # #######################################################################################################
+ #
+@@ -64,23 +67,167 @@
+ 
+ email:
+ 
++  126 Mail:
++    domains:
++      - mail.126.com
++
++  163 Mail:
++    domains:
++      - mail.163.com
++
++  2degrees:
++    domains:
++      - webmail.2degreesbroadband.co.nz
++
++  Adam Internet:
++    domains:
++      - webmail.adam.com.au
++
++  AOL Mail:
++    domains:
++      - mail.aol.com
++
++  Beeline:
++    domains:
++      - post.ru
++
++  Bigpond:
++    domains:
++      - webmail.bigpond.com
++      - webmail2.bigpond.com
++      - email.telstra.com
++      - basic.messaging.bigpond.com
++
++  Commander:
++    domains:
++      - webmail.commander.net.au
++
++  Daum Mail:
++    domains:
++      - mail2.daum.net
++      - mail.daum.net
++
++  Dodo:
++    domains:
++      - webmail.dodo.com.au
++
++  E1.ru:
++    domains:
++      - mail.e1.ru
++
++  Freenet:
++    domains:
++      - webmail.freenet.de
++
+   Gmail:
+     domains:
+       - mail.google.com
++      - inbox.google.com
++
++  iiNet:
++    domains:
++      - webmail.iinet.net.au
++      - mail.iinet.net.au
++
++  Inbox.com:
++    domains:
++      - inbox.com
++
++  iPrimus:
++    domains:
++      - webmail.iprimus.com.au
++
++  Mail.ru:
++    domains:
++      - e.mail.ru
++      - touch.mail.ru
++
++  Mastermail:
++    domains:
++      - mastermail.ru
++      - m.mastermail.ru
++
++  Mynet Mail:
++    domains:
++      - mail.mynet.com
++
++  Naver Mail:
++    domains:
++      - mail.naver.com
++
++  Netspace:
++    domains:
++      - webmail.netspace.net.au
++
++  Optus Zoo:
++    domains:
++      - webmail.optuszoo.com.au
++      - webmail.optusnet.com.au
++
++  Orange Webmail:
++    domains:
++      - orange.fr/webmail
+ 
+   Outlook.com:
+     domains:
+       - mail.live.com
++      - outlook.live.com
++
++  QIP:
++    domains:
++      - mail.qip.ru
++
++  QQ Mail:
++    domains:
++      - mail.qq.com
++      - exmail.qq.com
++
++  Rambler:
++    domains:
++      - mail.rambler.ru 
++
++  Seznam Mail:
++    domains:
++      - email.seznam.cz
++
++  Sibmail:
++    domains:
++      - sibmail.com
++
++  Ukr.net:
++    domains:
++      - mail.ukr.net
++
++  Virgin:
++    domains:
++      - webmail.virginbroadband.com.au
++
++  Vodafone:
++    domains:
++      - webmail.vodafone.co.nz
++
++  Westnet:
++    domains:
++      - webmail.westnet.com.au
++
++  Yandex:
++    domains:
++      - mail.yandex.ru
++      - mail.yandex.com
++      - mail.yandex.kz
++      - mail.yandex.ua
++      - mail.yandex.by
+ 
+   Yahoo! Mail:
+     domains:
+       - mail.yahoo.net
+       - mail.yahoo.com
+       - mail.yahoo.co.uk
++      - mail.yahoo.co.jp
+ 
+-  Orange Webmail:
++  Zoho:
+     domains:
+-      - orange.fr/webmail
++      - mail.zoho.com
+ 
+ # #######################################################################################################
+ #
+@@ -92,6 +239,9 @@
+     domains:
+       - facebook.com
+       - fb.me
++      - m.facebook.com
++      - l.facebook.com
++      - lm.facebook.com
+ 
+   Qzone:
+     domains:
+@@ -106,6 +256,20 @@
+       - twitter.com
+       - t.co
+ 
++  Instagram:
++    domains:
++      - instagram.com
++      - l.instagram.com
++
++  Youtube:
++    domains:
++      - youtube.com
++      - youtu.be
++
++  Vimeo:
++    domains:
++      - vimeo.com
++
+   Renren:
+     domains:
+       - renren.com
+@@ -117,6 +281,7 @@
+   LinkedIn:
+     domains:
+       - linkedin.com
++      - lnkd.in
+ 
+   Bebo:
+     domains:
+@@ -124,7 +289,9 @@
+ 
+   Vkontakte:
+     domains:
++      - m.vk.com
+       - vk.com
++      - away.vk.com
+       - vkontakte.ru
+ 
+   Tagged:
+@@ -133,7 +300,7 @@
+ 
+   Orkut:
+     domains:
+-      - orkut.com 
++      - orkut.com
+ 
+   Myspace:
+     domains:
+@@ -187,6 +354,7 @@
+   Odnoklassniki:
+     domains:
+       - odnoklassniki.ru
++      - ok.ru
+ 
+   Viadeo:
+     domains:
+@@ -194,7 +362,7 @@
+ 
+   Flickr:
+     domains:
+-      - flickr.com 
++      - flickr.com
+ 
+   WeeWorld:
+     domains:
+@@ -226,7 +394,7 @@
+ 
+   Skyrock:
+     domains:
+-      - skyrock.com 
++      - skyrock.com
+ 
+   BlackPlanet:
+     domains:
+@@ -263,6 +431,8 @@
+   Pinterest:
+     domains:
+       - pinterest.com
++      - pinterest.ca
++      - pinterest.com.au
+ 
+   Plaxo:
+     domains:
+@@ -278,7 +448,7 @@
+ 
+   XING:
+     domains:
+-      - xing.com 
++      - xing.com
+ 
+   Taringa!:
+     domains:
+@@ -287,6 +457,7 @@
+   Tumblr:
+     domains:
+       - tumblr.com
++      - t.umblr.com
+ 
+   Nasza-klasa.pl:
+     domains:
+@@ -310,7 +481,7 @@
+ 
+   Buzznet:
+     domains:
+-      - wayn.com
++      - buzznet.com
+ 
+   Multiply:
+     domains:
+@@ -349,6 +520,67 @@
+       - weibo.com
+       - t.cn
+ 
++  Delicious:
++    domains:
++      - delicious.com
++
++  Pocket:
++    domains:
++      - getpocket.com
++
++  ITU Sozluk:
++    domains:
++      - itusozluk.com
++
++  Instela:
++    domains:
++      - instela.com
++
++  Eksi Sozluk:
++    domains:
++      - Sozluk.com
++      - sourtimes.org
++
++  Uludag Sozluk:
++    domains:
++      - uludagsozluk.com
++      - ulusozluk.com
++
++  Inci Sozluk:
++    domains:
++      - inci.sozlukspot.com
++      - incisozluk.com
++      - incisozluk.cc
++
++  Hocam.com:
++    domains:
++      - hocam.com
++
++  Donanimhaber:
++    domains:
++      - donanimhaber.com
++
++  Disqus:
++    domains:
++      - redirect.disqus.com
++      - disq.us
++      - disqus.com
++
++  Quora:
++    domains:
++      - quora.com
++
++  Skype:
++    domains:
++      - web.skype.com
++
++  WhatsApp:
++    domains:
++      - web.whatsapp.com
++
++  Whirlpool:
++    domains:
++      - forums.whirlpool.net.au
+ 
+ # #######################################################################################################
+ #
+@@ -357,24 +589,44 @@
+ search:
+ 
+   1.cz:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - 1.cz
+ 
+   # 123people TODO
+ 
++  1&1:
++    parameters:
++      - q
++    domains:
++      - search.1and1.com
++
+   1und1:
+-    parameters: 
++    parameters:
+       - su
+     domains:
+       - search.1und1.de
+ 
++  2gis:
++    domains:
++      - 2gis.ru
++      - www.2gis.ru
++      - link.2gis.ru
++      - www.link.2gis.ru
++
++  360.cn:
++    parameters:
++      - q
++    domains:
++      - so.360.cn
++      - www.so.com
++
+   Abacho:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+-      - www.abacho.de 
++      - www.abacho.de
+       - www.abacho.com
+       - www.abacho.co.uk
+       - www.se.abacho.com
+@@ -386,45 +638,45 @@
+       - www.abacho.it
+ 
+   ABCsøk:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - abcsolk.no
+       - verden.abcsok.no
+ 
+   Acoon:
+-    parameters: 
++    parameters:
+       - begriff
+     domains:
+       - www.acoon.de
+ 
+   Alexa:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - alexa.com
+       - search.toolbars.alexa.com
+ 
+   Alice Adsl:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - rechercher.aliceadsl.fr
+ 
+   AllTheWeb:
+-    parameters: 
++    parameters:
+       - q
+-    domains: 
++    domains:
+       - www.alltheweb.com
+ 
+   all.by:
+-    parameters: 
++    parameters:
+       - query
+     domains:
+       - all.by
+ 
+   Altavista:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - www.altavista.com
+@@ -435,18 +687,28 @@
+       - be-nl.altavista.com
+       - be-fr.altavista.com
+ 
++  Amazon:
++    parameters:
++      - keywords
++    domains:
++      - amazon.com
++      - www.amazon.com
++
+   AOL:
+-    parameters: 
++    parameters:
+       - q
++      - query
+     domains:
+       - search.aol.com
+       - search.aol.it
+       - aolsearch.aol.com
++      - aolsearch.com
+       - www.aolrecherche.aol.fr
+       - www.aolrecherches.aol.fr
+       - www.aolimages.aol.fr
+       - aim.search.aol.com
+       - www.recherche.aol.fr
++      - recherche.aol.fr
+       - find.web.aol.com
+       - recherche.aol.ca
+       - aolsearch.aol.co.uk
+@@ -465,13 +727,13 @@
+       - search-intl.netscape.com
+ 
+   Apollo Latvia:
+-    parameters: 
++    parameters:
+       - q
+-    domains: 
++    domains:
+       - apollo.lv/portal/search/
+ 
+   APOLL07:
+-    parameters: 
++    parameters:
+       - query
+     domains:
+       - apollo7.de
+@@ -484,35 +746,36 @@
+       - www.apontador.com.br
+ 
+   Aport:
+-    parameters: 
++    parameters:
+       - r
+     domains:
+       - sm.aport.ru
+ 
+   arama:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - arama.com
+ 
+   Arcor:
+-    parameters: 
++    parameters:
+       - Keywords
+     domains:
+       - www.arcor.de
+ 
+   Arianna:
+-    parameters: 
++    parameters:
+       - query
+     domains:
+       - arianna.libero.it
+       - www.arianna.com
+ 
+   Ask:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - ask.com
++      - www.ask.com
+       - web.ask.com
+       - int.ask.com
+       - mws.ask.com
+@@ -528,28 +791,34 @@
+       - www.search-results.com
+       - int.search-results.com
+ 
++  Ask Toolbar:
++    parameters:
++      - searchfor
++    domains:
++      - search.tb.ask.com
++
+   Atlas:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - searchatlas.centrum.cz
+ 
+   Austronaut:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - www2.austronaut.at
+       - www1.astronaut.at
+ 
+   Babylon:
+-    parameters: 
++    parameters:
+       - q
+-    domains: 
++    domains:
+      - search.babylon.com
+      - searchassist.babylon.com
+ 
+   Baidu:
+-    parameters: 
++    parameters:
+       - wd
+       - word
+       - kw
+@@ -561,9 +830,10 @@
+       - tieba.baidu.com
+       - news.baidu.com
+       - web.gougou.com
++      - m.baidu.com
+ 
+   Biglobe:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - cgi.search.biglobe.ne.jp
+@@ -612,6 +882,12 @@
+     domains:
+       - search.bluewin.ch
+ 
++  British Telecommunications:
++    parameters:
++      - p
++    domains:
++      - search.bt.com
++
+   canoe.ca:
+     parameters:
+       - q
+@@ -625,6 +901,12 @@
+       - serach.centrum.cz
+       - morfeo.centrum.cz
+ 
++  Certified-Toolbar:
++    parameters:
++      - q
++    domains:
++      - search.certified-toolbar.com
++
+   Charter:
+     parameters:
+       - q
+@@ -674,6 +956,12 @@
+       - daemon-search.com
+       - my.daemon-search.com
+ 
++  Dalesearch:
++    parameters:
++      - q
++    domains:
++      - www.dalesearch.com
++
+   DasOertliche:
+     parameters:
+       - kw
+@@ -717,7 +1005,13 @@
+       - dmoz.org
+       - editors.dmoz.org
+ 
+-  DuckDuckGoL:
++  Dodo:
++    parameters:
++      - q
++    domains:
++      - google.dodo.com.au
++
++  DuckDuckGo:
+     parameters:
+       - q
+     domains:
+@@ -799,6 +1093,18 @@
+     domains:
+       - recherche.francite.com
+ 
++  Finderoo:
++    parameters:
++      - q
++    domains:
++      - www.finderoo.com
++
++  Findwide:
++    parameters:
++      - k
++    domains:
++      - search.findwide.com
++
+   Fireball:
+     parameters:
+       - q
+@@ -852,6 +1158,12 @@
+     domains:
+       - suche.freenet.de
+ 
++  Freshweather:
++    parameters:
++      - q
++    domains:
++      - www.fresh-weather.com
++
+   FriendFeed:
+     parameters:
+       - q
+@@ -870,6 +1182,12 @@
+     domains:
+       - geona.net
+ 
++  Genieo:
++    parameters:
++      - q
++    domains:
++      - search.genieo.com
++
+   Gigablast:
+     parameters:
+       - q
+@@ -877,6 +1195,13 @@
+       - www.gigablast.com
+       - dir.gigablast.com
+ 
++  Globososo:
++    parameters:
++      - q
++    domains:
++      - searches.globososo.com
++      - search.globososo.com
++
+   GMX:
+     parameters:
+       - su
+@@ -1003,7 +1328,6 @@
+       - www.google.jo
+       - www.google.co.jp
+       - www.google.co.ke
+-      - www.google.com.kh
+       - www.google.ki
+       - www.google.kg
+       - www.google.co.kr
+@@ -1084,6 +1408,7 @@
+       - www.google.tm
+       - www.google.to
+       - www.google.com.tn
++      - www.google.tn
+       - www.google.com.tr
+       - www.google.tt
+       - www.google.com.tw
+@@ -1200,7 +1525,6 @@
+       - google.jo
+       - google.co.jp
+       - google.co.ke
+-      - google.com.kh
+       - google.ki
+       - google.kg
+       - google.co.kr
+@@ -1302,6 +1626,7 @@
+       - google.co.za
+       - google.co.zm
+       - google.co.zw
++      - google.tn
+       # powered by Google
+       - search.avg.com
+       - isearch.avg.com
+@@ -1414,7 +1739,6 @@
+       - blogsearch.google.com.hk
+       - blogsearch.google.com.jm
+       - blogsearch.google.com.kh
+-      - blogsearch.google.com.kh
+       - blogsearch.google.com.kw
+       - blogsearch.google.com.lb
+       - blogsearch.google.com.lc
+@@ -1616,7 +1940,6 @@
+       - google.com.hk/imgres
+       - google.com.jm/imgres
+       - google.com.kh/imgres
+-      - google.com.kh/imgres
+       - google.com.kw/imgres
+       - google.com.lb/imgres
+       - google.com.lc/imgres
+@@ -1813,7 +2136,6 @@
+       - images.google.com.hk
+       - images.google.com.jm
+       - images.google.com.kh
+-      - images.google.com.kh
+       - images.google.com.kw
+       - images.google.com.lb
+       - images.google.com.lc
+@@ -1933,7 +2255,6 @@
+       - images.google.us
+       - images.google.vg
+       - images.google.vu
+-      - images.google.ws
+ 
+   Google News:
+     parameters:
+@@ -2015,7 +2336,6 @@
+       - news.google.com.hk
+       - news.google.com.jm
+       - news.google.com.kh
+-      - news.google.com.kh
+       - news.google.com.kw
+       - news.google.com.lb
+       - news.google.com.lc
+@@ -2217,7 +2537,6 @@
+       - google.com.hk/products
+       - google.com.jm/products
+       - google.com.kh/products
+-      - google.com.kh/products
+       - google.com.kw/products
+       - google.com.lb/products
+       - google.com.lc/products
+@@ -2414,7 +2733,6 @@
+       - www.google.com.hk/products
+       - www.google.com.jm/products
+       - www.google.com.kh/products
+-      - www.google.com.kh/products
+       - www.google.com.kw/products
+       - www.google.com.lb/products
+       - www.google.com.lc/products
+@@ -2561,7 +2879,7 @@
+       - www.highbeam.com
+ 
+   Hit-Parade:
+-    parameters: 
++    parameters:
+       - p7
+     domains:
+       - req.-hit-parade.com
+@@ -2605,6 +2923,21 @@
+     domains:
+       - www.ilse.nl
+ 
++  Inbox.com:
++    parameters:
++      - q
++    domains:
++      - inbox.com/search/
++
++  Indeed:
++    domains:
++      - de.indeed.com
++      - at.indeed.com
++      - fr.indeed.com
++      - it.indeed.com
++      - ch.indeed.com
++      - au.indeed.com
++
+   InfoSpace:
+     parameters:
+       - q
+@@ -2624,6 +2957,13 @@
+       - search.searchcompletion.com
+       - clusty.com
+ 
++  Flyingbird:
++    parameters:
++      - q
++    domains:
++      - inspsearch.com
++      - viview.inspsearch.com
++
+   Interia:
+     parameters:
+       - q
+@@ -2636,6 +2976,12 @@
+     domains:
+       - start.iplay.com
+ 
++  I.ua:
++    parameters:
++      - q
++    domains:
++      - search.i.ua
++
+   IXquick:
+     parameters:
+       - query
+@@ -2685,6 +3031,12 @@
+     domains:
+       - www.kvasir.no
+ 
++  kununu:
++    parameters:
++      - q
++    domains:
++      - kununu.com
++
+   Latne:
+     parameters:
+       - q
+@@ -2698,6 +3050,18 @@
+       - www.toile.com
+       - web.toile.com
+ 
++  Lilo:
++    parameters:
++      - q
++    domains:
++      - search.lilo.org
++
++  Liveinternet:
++    parameters:
++      - q
++    domains:
++      - liveinternet.ru
++
+   Looksmart:
+     parameters:
+       - key
+@@ -2728,6 +3092,8 @@
+     parameters:
+       - q
+     domains:
++      - mail.ru
++      - m.mail.ru
+       - go.mail.ru
+ 
+   Mamma:
+@@ -2737,6 +3103,18 @@
+       - www.mamma.com
+       - mamma75.mamma.com
+ 
++  Marktplaats:
++    parameters:
++      - query
++    domains:
++      - www.marktplaats.nl
++
++  Maxwebsearch:
++    parameters:
++      - query
++    domains:
++      - maxwebsearch.com
++
+   Meta:
+     parameters:
+       - q
+@@ -2771,12 +3149,27 @@
+       - www.meinestadt.de
+ 
+   Mister Wong:
+-    parameters: 
++    parameters:
+       - Keywords
+     domains:
+       - www.mister-wong.com
+       - www.mister-wong.de
+ 
++  Monster:
++    parameters:
++      - q
++    domains:
++      - www.monster.be
++      - www.monster.cz
++      - www.monster.de
++      - www.monster.fi
++      - www.monster.fr
++      - www.monster.ie
++      - www.monster.it
++      - www.monster.lu
++      - www.monster.ch
++      - www.monster.co.uk
++
+   Monstercrawler:
+     parameters:
+       - qry
+@@ -2802,6 +3195,7 @@
+       - searchfor
+       - searchFor
+     domains:
++      - mysearch.com
+       - www.mysearch.com
+       - ms114.mysearch.com
+       - ms146.mysearch.com
+@@ -2828,6 +3222,13 @@
+     domains:
+       - search.naver.com
+ 
++  Naver Images:
++    parameters:
++      - query
++    domains:
++      - image.search.naver.com
++      - imagesearch.naver.com
++
+   Needtofind:
+     parameters:
+       - searchfor
+@@ -2873,9 +3274,11 @@
+   Orange:
+     parameters:
+       - q
++      - kw
+     domains:
+       - busca.orange.es
+       - search.orange.co.uk
++      - lemoteur.orange.fr
+ 
+   Paperball:
+     parameters:
+@@ -2905,7 +3308,7 @@
+     parameters:
+       - q
+     domains:
+-      - www.plazoo.com
++      - poisk.ru
+ 
+   PriceRunner:
+     parameters:
+@@ -2914,7 +3317,7 @@
+       - www.pricerunner.co.uk
+ 
+   qip:
+-    parameters: 
++    parameters:
+       - query
+     domains:
+       - search.qip.ru
+@@ -2928,6 +3331,13 @@
+       - www.qualigo.de
+       - www.qualigo.nl
+ 
++  Qwant:
++    parameters:
++      - q
++    domains:
++      - www.qwant.com
++      - lite.qwant.com
++
+   Rakuten:
+     parameters:
+       - qt
+@@ -2935,7 +3345,7 @@
+       - websearch.rakuten.co.jp
+ 
+   Rambler:
+-    parameters: 
++    parameters:
+      - query
+      - words
+     domains:
+@@ -2943,8 +3353,7 @@
+ 
+   RPMFind:
+     parameters:
+-      - rpmfind.net
+-      - fr2.rpmfind.net
++      - query
+     domains:
+       - rpmfind.net
+       - fr2.rpmfind.net
+@@ -2963,6 +3372,12 @@
+ 
+   # Add Scour.com
+ 
++  Search This:
++    parameters:
++      - q
++    domains:
++      - www.searchthis.com
++
+   Search.com:
+     parameters:
+       - q
+@@ -3013,11 +3428,20 @@
+     domains:
+       - www.skynet.be
+ 
++  The Smart Search:
++    parameters:
++      - q
++    domains:
++      - thesmartsearch.net
++      - www.thesmartsearch.net
++
+   Sogou:
+     parameters:
+       - query
++      - w
+     domains:
+       - www.sougou.com
++      - www.soso.com
+ 
+   Softonic:
+     parameters:
+@@ -3025,11 +3449,30 @@
+     domains:
+       - search.softonic.com
+ 
+-  soso.com:
++  SoSoDesk:
+     parameters:
+-      - w
++      - q
+     domains:
+-      - www.soso.com
++      - sosodesktop.com
++      - search.sosodesktop.com
++
++  Shenma:
++    parameters:
++      - q
++    domains:
++      - so.m.sm.cn
++      - yz.m.sm.cn
++      - m.sm.cn
++      - quark.sm.cn
++      - m.sp.sm.cn
++      - m.yz2.sm.cn
++      - m.yz.sm.cn
++
++  Snapdo:
++    parameters:
++      - q
++    domains:
++      - search.snapdo.com
+ 
+   Startpagina:
+     parameters:
+@@ -3043,6 +3486,16 @@
+     domains:
+       - www.startsiden.no
+ 
++  StepStone:
++    domains:
++      - www.stepstone.de
++      - www.stepstone.at
++      - www.stepstone.be
++      - www.stepstone.fr
++      - www.stepstone.nl
++      - www.stepstone.dk
++      - www.stepstone.se
++
+   suche.info:
+     parameters:
+       - q
+@@ -3068,11 +3521,17 @@
+       - www.talktalk.co.uk
+ 
+   Technorati:
+-    parameters: 
++    parameters:
+       - q
+     domains:
+       - technorati.com
+ 
++  Telstra:
++    parameters:
++      - find
++    domains:
++      - search.media.telstra.com.au
++
+   Teoma:
+     parameters:
+       - q
+@@ -3135,12 +3594,24 @@
+     domains:
+       - www.trusted--search.com
+ 
++  Tut.by:
++    parameters:
++      - query
++    domains:
++      - search.tut.by
++
+   Twingly:
+     parameters:
+       - q
+     domains:
+       - www.twingly.com
+ 
++  UKR.net:
++    parameters:
++      - q
++    domains:
++      - search.ukr.net
++
+   uol.com.br:
+     parameters:
+       - q
+@@ -3179,6 +3650,7 @@
+   Voila:
+     parameters:
+       - rdata
++      - kw
+     domains:
+       - search.ke.voila.fr
+       - www.lemoteur.fr
+@@ -3199,7 +3671,7 @@
+       - www.walhello.nl
+ 
+   Web.de:
+-    parameters: 
++    parameters:
+       - su
+     domains:
+       - suche.web.de
+@@ -3266,6 +3738,8 @@
+       - au.yahoo.com
+       - br.search.yahoo.com
+       - br.yahoo.com
++      - ca.search.yahoo.com
++      - ca.yahoo.com
+       - cade.searchde.yahoo.com
+       - cade.yahoo.com
+       - chinese.searchinese.yahoo.com
+@@ -3279,13 +3753,15 @@
+       - es.search.yahoo.com
+       - es.yahoo.com
+       - espanol.searchpanol.yahoo.com
+-      - espanol.searchpanol.yahoo.com
+-      - espanol.yahoo.com
+       - espanol.yahoo.com
+       - fr.search.yahoo.com
+       - fr.yahoo.com
++      - hk.search.yahoo.com
++      - hk.yahoo.com
+       - ie.search.yahoo.com
+       - ie.yahoo.com
++      - in.search.yahoo.com
++      - in.yahoo.com
+       - it.search.yahoo.com
+       - it.yahoo.com
+       - kr.search.yahoo.com
+@@ -3299,17 +3775,18 @@
+       - one.cn.yahoo.com
+       - one.searchn.yahoo.com
+       - qc.search.yahoo.com
+-      - qc.search.yahoo.com
+-      - qc.search.yahoo.com
+-      - qc.yahoo.com
+       - qc.yahoo.com
+-      - se.search.yahoo.com
++      - ru.search.yahoo.com
++      - ru.yahoo.com
+       - se.search.yahoo.com
+       - se.yahoo.com
+       - search.searcharch.yahoo.com
+-      - search.yahoo.com
++      - tw.search.yahoo.com
++      - tw.yahoo.com
+       - uk.search.yahoo.com
+       - uk.yahoo.com
++      - us.search.yahoo.com
++      - us.yahoo.com
+       - www.yahoo.co.jp
+       - search.yahoo.co.jp
+       # powered by Yahoo
+@@ -3338,9 +3815,15 @@
+       - yandex.ru
+       - yandex.ua
+       - yandex.com
++      - yandex.by
+       - www.yandex.ru
+       - www.yandex.ua
+       - www.yandex.com
++      - www.yandex.by
++      - clck.yandex.ru
++      - clck.yandex.ua
++      - clck.yandex.com
++      - clck.yandex.by
+ 
+   Yandex Images:
+     parameters:
+@@ -3349,11 +3832,12 @@
+       - images.yandex.ru
+       - images.yandex.ua
+       - images.yandex.com
++      - images.yandex.by
+ 
+   Yasni:
+     parameters:
+       - query
+-    domains: 
++    domains:
+       - www.yasni.de
+       - www.yasni.com
+       - www.yasni.co.uk
+@@ -3366,7 +3850,7 @@
+     domains:
+       - www.yatedo.com
+       - www.yatedo.fr
+-    
++
+   # Add Yellowmap:
+ 
+   Yippy:
+@@ -3415,3 +3899,227 @@
+       - q
+     domains:
+       - zoohoo.cz
++
++
++
++# #######################################################################################################
++#
++# PAID MEDIA
++
++paid:
++
++  Acuity Ads:
++    domains:
++      - acuityplatform.com
++
++  Adform:
++    domains:
++      - adform.net
++
++  ADFOX:
++    domains:
++      - adfox.ru
++      - www.adfox.ru
++      - ads.adfox.ru
++      - www.ads.adfox.ru
++
++  Adition:
++    domains:
++      - adition.com
++
++  AdNET:
++    domains:
++      - adnet.de
++
++  AdRoll:
++    domains:
++      - adroll.com
++
++  AdSpirit:
++    domains:
++      - adspirit.de
++      - rtbcity.com
++      - plusperformance.com
++
++  AppNexus:
++    domains:
++      - ib.adnxs.com
++      - adnxs.com
++      - 247realmedia.com
++
++  AudienceScience:
++    domains:
++      - wunderloop.net
++
++  BidSwitch:
++    domains:
++      - bidswitch.net
++
++  Casale Media:
++    domains:
++      - casalemedia.com
++
++  Criteo:
++    domains:
++      - cas.jp.as.criteo.com
++      - cas.criteo.com
++
++  Doubleclick:
++    domains:
++      - ad.doubleclick.net
++      - ad-apac.doubleclick.net
++      - s0.2mdn.net
++      - s1.2mdn.net
++      - dp.g.doubleclick.net
++      - pubads.g.doubleclick.net
++
++  Eyeota:
++    domains:
++      - eyeota.net
++
++  Flashtalking:
++    domains:
++      - flashtalking.com
++      - servedby.flashtalking.com
++
++  Fluct:
++    domains:
++      - adingo.jp
++
++  Google:
++    domains:
++      - www.googleadservices.com
++      - partner.googleadservices.com
++      - googleads.g.doubleclick.net
++      - tpc.googlesyndication.com
++      - googleadservices.com
++      - imasdk.googleapis.com
++
++  LifeStreet:
++    domains:
++      - lfstmedia.com
++
++  LowerMyBills:
++    parameters:
++      - leadid
++    domains:
++      - lowermybills.com
++
++  Jivox:
++    domains:
++      - jivox.com
++
++  MicroAd:
++    domains:
++      - microad.jp
++
++  Mixpo:
++    domains:
++      - mixpo.com
++
++  Mozo:
++    domains:
++      - mozo.com.au
++      - a.mozo.com.au
++
++  Neustar AdAdvisor:
++    domains:
++      - adadvisor.net
++
++  ONE by AOL:
++    domains:
++      - nexage.com
++
++  OpenX:
++    domains:
++      - us-ads.openx.net
++      - openx.net
++      - servedbyopenx.com
++      - openxenterprise.com
++
++  Outbrain:
++    domains:
++      - paid.outbrain.com
++
++  Plista:
++    domains:
++      - farm.plista.com
++
++  Price.ru:
++    domains:
++      - price.ru
++      - v.price.ru
++
++  PubMatic:
++    domains:
++      - sshowads.pubmatic.com
++
++  Rubicon Project:
++    domains:
++      - optimized-by.rubiconproject.com
++
++  Sizmek:
++    domains:
++      - bs.serving-sys.com
++
++  Sociomantic Labs:
++    domains:
++      - sociomantic.com
++
++  Sonobi:
++    domains:
++      - sonobi.com
++
++  Sovrn:
++    domains:
++      - lijit.com
++
++  SteelHouse:
++    domains:
++      - steelhousemedia.com
++
++  StickyADS.tv:
++    domains:
++      - stickyadstv.com
++      - sfx.stickyadstv.com
++
++  Taboola:
++    domains:
++      - trc.taboola.com
++      - api.taboola.com
++      - taboola.com
++
++  Torg.Mail.ru:
++    domains:
++      - torg.mail.ru
++
++  Tribal Fusion:
++    domains:
++      - cdnx.tribalfusion.com
++
++  White Pages:
++    domains:
++      - www.whitepages.com.au
++      - mobile.whitepages.com.au
++
++  Yandex.Direct:
++    domains:
++      - an.yandex.ru
++      - yabs.yandex.ru
++      - yabs.yandex.ua
++      - yabs.yandex.com
++      - yabs.yandex.by
++
++  Yandex.Market:
++    domains:
++      - market.yandex.ru
++      - m.market.yandex.ru
++
++  Yieldmo:
++    domains:
++      - yieldmo.com
++
++  ZEDO:
++    domains:
++      - zedo.com
++      - z1.zedo.com

+ 30 - 0
.yarn/patches/retry-request-npm-7.0.2-a41087680c.patch

@@ -0,0 +1,30 @@
+diff --git a/index.js b/index.js
+index 2fae107d03b30aff9320d135ec79c049c51f298a..32ec707ddbf8937e3e130c3deac1060c308bf439 100644
+--- a/index.js
++++ b/index.js
+@@ -1,6 +1,6 @@
+ 'use strict';
+ 
+-const {PassThrough} = require('stream');
++const {PassThrough, pipeline} = require('stream');
+ const extend = require('extend');
+ 
+ let debug = () => {};
+@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
+         .on('complete', (...params) => handleFinish(params))
+         .on('finish', (...params) => handleFinish(params));
+ 
+-      requestStream.pipe(delayStream);
++      pipeline(requestStream, delayStream, () => {});
+     } else {
+       activeRequest = opts.request(requestOpts, onResponse);
+     }
+@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
+     // No more attempts need to be made, just continue on.
+     if (streamMode) {
+       retryStream.emit('response', response);
+-      delayStream.pipe(retryStream);
++      pipeline(delayStream, retryStream, () => {});
+       requestStream.on('error', err => {
+         retryStream.destroy(err);
+       });

+ 30 - 0
.yarn/patches/retry-request-npm-8.0.2-448ad084c8.patch

@@ -0,0 +1,30 @@
+diff --git a/index.js b/index.js
+index 298a351097d70a7fb005b6961f4d58247e391d8f..6a809ace0349d40cb2b6732ad66fd8ad208698ca 100644
+--- a/index.js
++++ b/index.js
+@@ -1,6 +1,6 @@
+ 'use strict';
+ 
+-const {PassThrough} = require('stream');
++const {PassThrough, pipeline} = require('stream');
+ const extend = require('extend');
+ 
+ let debug = () => {};
+@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
+         .on('complete', (...params) => handleFinish(params))
+         .on('finish', (...params) => handleFinish(params));
+ 
+-      requestStream.pipe(delayStream);
++      pipeline(requestStream, delayStream, () => {});
+     } else {
+       activeRequest = opts.request(requestOpts, onResponse);
+     }
+@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
+     // No more attempts need to be made, just continue on.
+     if (streamMode) {
+       retryStream.emit('response', response);
+-      delayStream.pipe(retryStream);
++      pipeline(delayStream, retryStream, () => {});
+       requestStream.on('error', err => {
+         retryStream.destroy(err);
+       });

+ 13 - 0
.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch

@@ -0,0 +1,13 @@
+diff --git a/lib/sandboxed_module.js b/lib/sandboxed_module.js
+index 1cd6743fe221cbe91ea92fea3707ed07a8a2ded3..46889217d96d5534a206549ae7bd97100e41c3e4 100644
+--- a/lib/sandboxed_module.js
++++ b/lib/sandboxed_module.js
+@@ -4,7 +4,7 @@ var Module = require('module');
+ var fs = require('fs');
+ var vm = require('vm');
+ var path = require('path');
+-var builtinModules = require('./builtin_modules.json');
++var builtinModules = Module.builtinModules || require('./builtin_modules.json');
+ var parent = module.parent;
+ var globalOptions = {};
+ var registeredBuiltInSourceTransformers = ['coffee'];

+ 57 - 0
.yarn/patches/send-npm-0.19.0.patch

@@ -0,0 +1,57 @@
+diff --git a/index.js b/index.js
+index 768f8ca..a882f4d 100644
+--- a/index.js
++++ b/index.js
+@@ -788,29 +788,29 @@ SendStream.prototype.stream = function stream (path, options) {
+   // pipe
+   var stream = fs.createReadStream(path, options)
+   this.emit('stream', stream)
+-  stream.pipe(res)
+-
+-  // cleanup
+-  function cleanup () {
+-    destroy(stream, true)
+-  }
+-
+-  // response finished, cleanup
+-  onFinished(res, cleanup)
+-
+-  // error handling
+-  stream.on('error', function onerror (err) {
+-    // clean up stream early
+-    cleanup()
+-
+-    // error
+-    self.onStatError(err)
+-  })
+-
+-  // end
+-  stream.on('end', function onend () {
+-    self.emit('end')
+-  })
++  Stream.pipeline(stream, res, err => { if (err) { self.onStatError(err) } else { self.emit('end') } })
++
++  // // cleanup
++  // function cleanup () {
++  //   destroy(stream, true)
++  // }
++  //
++  // // response finished, cleanup
++  // onFinished(res, cleanup)
++  //
++  // // error handling
++  // stream.on('error', function onerror (err) {
++  //   // clean up stream early
++  //   cleanup()
++  //
++  //   // error
++  //   self.onStatError(err)
++  // })
++  //
++  // // end
++  // stream.on('end', function onend () {
++  //   self.emit('end')
++  // })
+ }
+ 
+ /**

+ 57 - 0
.yarn/patches/teeny-request-npm-10.1.0.patch

@@ -0,0 +1,57 @@
+diff --git a/build/src/index.js b/build/src/index.js
+index a101736..a87f6b9 100644
+--- a/build/src/index.js
++++ b/build/src/index.js
+@@ -130,6 +130,9 @@ function createMultipartStream(boundary, multipart) {
+         }
+         else {
+             part.body.pipe(stream, { end: false });
++            part.body.on('error', (err) => {
++                stream.destroy(err);
++            });
+             part.body.on('end', () => {
+                 stream.write('\r\n');
+                 stream.write(finale);
+@@ -184,25 +187,25 @@ function teenyRequest(reqOpts, callback) {
+         // Stream mode
+         const requestStream = streamEvents(new stream_1.PassThrough());
+         // eslint-disable-next-line @typescript-eslint/no-explicit-any
+-        let responseStream;
+-        requestStream.once('reading', () => {
+-            if (responseStream) {
+-                (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+-            }
+-            else {
+-                requestStream.once('response', () => {
+-                    (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+-                });
+-            }
+-        });
++        // let responseStream;
++        // requestStream.once('reading', () => {
++        //     if (responseStream) {
++        //         (0, stream_1.pipeline)(responseStream, requestStream, () => { });
++        //     }
++        //     else {
++        //         requestStream.once('response', () => {
++        //             (0, stream_1.pipeline)(responseStream, requestStream, () => { });
++        //         });
++        //     }
++        // });
+         options.compress = false;
+         teenyRequest.stats.requestStarting();
+-        fetch(uri, options).then(res => {
++        (0, node_fetch_1.default)(uri, options).then(res => {
+-            teenyRequest.stats.requestFinished();
+-            responseStream = res.body;
+-            responseStream.on('error', (err) => {
+-                requestStream.emit('error', err);
+-            });
++            teenyRequest.stats.requestFinished(); (0, stream_1.pipeline)(res.body, requestStream, () => {});
++            // responseStream = res.body;
++            // responseStream.on('error', (err) => {
++            //     requestStream.emit('error', err);
++            // });
+             const response = fetchToRequestResponse(options, res);
+             requestStream.emit('response', response);
+         }, err => {

+ 58 - 0
.yarn/patches/teeny-request-npm-9.0.0-4d571e3c55.patch

@@ -0,0 +1,58 @@
+diff --git a/build/src/index.js b/build/src/index.js
+index af5d15e260e2a47588c7c536447fe84bd3f86136..2b63d0c0b1eb6595c7a0bb314c1df792454c1a72 100644
+--- a/build/src/index.js
++++ b/build/src/index.js
+@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
+         }
+         else {
+             part.body.pipe(stream, { end: false });
++            part.body.on('error', (err) => {
++                stream.destroy(err);
++            });
+             part.body.on('end', () => {
+                 stream.write('\r\n');
+                 stream.write(finale);
+@@ -168,25 +171,27 @@ function teenyRequest(reqOpts, callback) {
+         // Stream mode
+         const requestStream = streamEvents(new stream_1.PassThrough());
+         // eslint-disable-next-line @typescript-eslint/no-explicit-any
+-        let responseStream;
+-        requestStream.once('reading', () => {
+-            if (responseStream) {
+-                (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+-            }
+-            else {
+-                requestStream.once('response', () => {
+-                    (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+-                });
+-            }
+-        });
++        // let responseStream;
++        // requestStream.once('reading', () => {
++        //    if (responseStream) {
++        //        (0, stream_1.pipeline)(responseStream, requestStream, () => { });
++        //    }
++        //    else {
++        //        requestStream.once('response', () => {
++        //            (0, stream_1.pipeline)(responseStream, requestStream, () => { });
++        //        });
++        //    }
++        // });
++
++
+         options.compress = false;
+         teenyRequest.stats.requestStarting();
+         (0, node_fetch_1.default)(uri, options).then(res => {
+-            teenyRequest.stats.requestFinished();
+-            responseStream = res.body;
+-            responseStream.on('error', (err) => {
+-                requestStream.emit('error', err);
+-            });
++            teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
++        //    responseStream = res.body;
++        //    responseStream.on('error', (err) => {
++        //        requestStream.emit('error', err);
++        //    });
+             const response = fetchToRequestResponse(options, res);
+             requestStream.emit('response', response);
+         }, err => {

+ 81 - 0
.yarn/patches/thread-loader-npm-4.0.2-dab5735f54.patch

@@ -0,0 +1,81 @@
+diff --git a/dist/WorkerPool.js b/dist/WorkerPool.js
+index 4145779f08eefafd0c18394806b6409c595ac5bb..aa16dd6a0f463804455164493a1e75e80cdf656a 100644
+--- a/dist/WorkerPool.js
++++ b/dist/WorkerPool.js
+@@ -258,6 +258,19 @@ class PoolWorker {
+           finalCallback();
+           break;
+         }
++      case 'logMessage':
++        {
++          const {
++            data: { loggerName, methodName, args }
++          } = message;
++          const {
++            data: jobData
++          } = this.jobs[id];
++          const logger = jobData.getLogger(loggerName);
++          logger[methodName].apply(logger, args);
++          finalCallback();
++          break;
++        }
+       case 'emitWarning':
+         {
+           const {
+diff --git a/dist/index.js b/dist/index.js
+index 75cd30fb63dc864057c1afc866f43fc7cc0a8020..d834af6ce1e2cd4473c4ae1b325d63eb1190c17e 100644
+--- a/dist/index.js
++++ b/dist/index.js
+@@ -43,6 +43,7 @@ function pitch() {
+     sourceMap: this.sourceMap,
+     emitError: this.emitError,
+     emitWarning: this.emitWarning,
++    getLogger: this.getLogger,
+     loadModule: this.loadModule,
+     resolve: this.resolve,
+     getResolve: this.getResolve,
+diff --git a/dist/worker.js b/dist/worker.js
+index 8e67959e4b7c9fd0db116509b1459636ba13a097..aca94f1442906baf179ada8e6a56501bbf71c480 100644
+--- a/dist/worker.js
++++ b/dist/worker.js
+@@ -90,6 +90,22 @@ function writeJson(data) {
+   writePipeWrite(lengthBuffer);
+   writePipeWrite(messageBuffer);
+ }
++const LOGGER_METHODS = ['error', 'warn', 'info', 'log', 'debug', 'trace', 'group', 'groupEnd', 'groupCollapsed', 'status', 'clear', 'profile', 'profileEnd'];
++class Logger {
++  constructor(id, loggerName) {
++    this.id = id
++    this.loggerName = loggerName
++    for (const methodName of LOGGER_METHODS) {
++      this[methodName] = (...args) => {
++        writeJson({
++          type: 'logMessage',
++          id: this.id,
++          data: { loggerName, methodName, args }
++        })
++      }
++    }
++  }
++}
+ const queue = (0, _queue.default)(({
+   id,
+   data
+@@ -190,6 +206,7 @@ const queue = (0, _queue.default)(({
+           }
+           return options;
+         },
++        getLogger: (name) => new Logger(id, name),
+         emitWarning: warning => {
+           writeJson({
+             type: 'emitWarning',
+@@ -211,6 +228,9 @@ const queue = (0, _queue.default)(({
+           module._compile(code, filename); // eslint-disable-line no-underscore-dangle
+           return module.exports;
+         },
++        addDependency: filename => {
++          buildDependencies.push(filename);
++        },
+         addBuildDependency: filename => {
+           buildDependencies.push(filename);
+         },

+ 21 - 0
.yarnrc.yml

@@ -0,0 +1,21 @@
+approvedGitRepositories:
+  - "**"
+
+enableGlobalCache: false
+
+enableScripts: true
+
+nodeLinker: node-modules
+
+supportedArchitectures:
+  cpu:
+    - current
+    - arm64
+    - x64
+  libc:
+    - current
+    - glibc
+  os:
+    - current
+    - darwin
+    - linux

+ 2 - 1
dockerfiles/cypress/Dockerfile

@@ -4,7 +4,8 @@ ARG USER_GID=1000
 
 WORKDIR /overleaf
 
-RUN sed -i s/node:x:1000:/node:x:${USER_GID}:/ /etc/group \
+RUN corepack enable \
+&&  sed -i s/node:x:1000:/node:x:${USER_GID}:/ /etc/group \
 &&  sed -i s_node:x:1000:1000::/home/node:/bin/bash_node:x:${USER_UID}:${USER_GID}::/home/node:/bin/bash_ /etc/passwd \
 &&  chown -R node:node /home/node \
 &&  chown node:node /overleaf

+ 1 - 0
libraries/access-token-encryptor/buildscript.txt

@@ -5,5 +5,6 @@ access-token-encryptor
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/access-token-encryptor
 --pipeline-owner=32
 --public-repo=False

+ 2 - 2
libraries/access-token-encryptor/package.json

@@ -4,10 +4,10 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },

+ 1 - 0
libraries/fetch-utils/buildscript.txt

@@ -5,5 +5,6 @@ fetch-utils
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/fetch-utils
 --pipeline-owner=32
 --public-repo=False

+ 3 - 3
libraries/fetch-utils/package.json

@@ -4,10 +4,10 @@
   "description": "utilities for node-fetch",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },
@@ -26,7 +26,7 @@
     "typescript": "^5.0.4"
   },
   "dependencies": {
-    "@overleaf/o-error": "*",
+    "@overleaf/o-error": "workspace:*",
     "lodash": "^4.17.21",
     "node-fetch": "^2.7.0"
   }

+ 1 - 0
libraries/logger/buildscript.txt

@@ -5,5 +5,6 @@ logger
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/logger
 --pipeline-owner=32
 --public-repo=False

+ 5 - 5
libraries/logger/package.json

@@ -10,17 +10,17 @@
   "license": "AGPL-3.0-only",
   "version": "3.1.1",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
     "@google-cloud/logging-bunyan": "^5.1.0",
-    "@overleaf/fetch-utils": "*",
-    "@overleaf/o-error": "*",
+    "@overleaf/fetch-utils": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
     "bunyan": "^1.8.14"
   },
   "devDependencies": {
@@ -34,6 +34,6 @@
     "typescript": "^5.0.4"
   },
   "peerDependencies": {
-    "@overleaf/metrics": "*"
+    "@overleaf/metrics": "workspace:*"
   }
 }

+ 1 - 0
libraries/metrics/buildscript.txt

@@ -5,5 +5,6 @@ metrics
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/metrics
 --pipeline-owner=32
 --public-repo=False

+ 4 - 4
libraries/metrics/package.json

@@ -34,12 +34,12 @@
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
-    "test:acceptance": "mocha --recursive --exit --grep=$MOCHA_GREP test/acceptance",
-    "test": "npm run lint && npm run types:check && npm run test:unit",
-    "test:ci": "npm run test:unit",
+    "test:acceptance": "mocha --recursive --exit --grep=${MOCHA_GREP:-} test/acceptance",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
+    "test:ci": "yarn run test:unit",
     "types:check": "tsc --noEmit"
   },
   "peerDependencies": {
-    "@overleaf/logger": "*"
+    "@overleaf/logger": "workspace:*"
   }
 }

+ 1 - 0
libraries/mongo-utils/buildscript.txt

@@ -5,6 +5,7 @@ mongo-utils
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/mongo-utils
 --pipeline-owner=32
 --public-repo=False
 --tsconfig-no-implicit-any=True

+ 2 - 2
libraries/mongo-utils/package.json

@@ -4,11 +4,11 @@
   "description": "utilities to help working with mongo",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "types:check": "tsc --noEmit"
   },
   "author": "Overleaf (https://www.overleaf.com)",

+ 1 - 0
libraries/o-error/buildscript.txt

@@ -5,5 +5,6 @@ o-error
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/o-error
 --pipeline-owner=32
 --public-repo=False

+ 3 - 3
libraries/o-error/package.json

@@ -17,11 +17,11 @@
     "index.cjs"
   ],
   "scripts": {
-    "build": "npm run --silent test",
+    "build": "yarn run --silent test",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test": "npm run lint && npm run types:check && npm run test:unit",
-    "test:ci": "npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },

+ 1 - 1
libraries/object-persistor/README.md

@@ -304,7 +304,7 @@ In order to prevent accidental deletion from outside this mechanism, an event-ba
 Contributions should pass lint, formatting and unit test checks. To run these, use
 
 ```
-npm run test
+yarn run test
 ```
 
 There are no acceptance tests in this module, but https://github.com/overleaf/filestore/ contains a comprehensive set of acceptance tests that use this module. These should also pass, with the changes.

+ 1 - 0
libraries/object-persistor/buildscript.txt

@@ -5,6 +5,7 @@ object-persistor
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/object-persistor
 --pipeline-owner=32
 --public-repo=False
 --tsconfig-no-implicit-any=True

+ 6 - 6
libraries/object-persistor/package.json

@@ -4,11 +4,11 @@
   "description": "Module for storing objects in multiple backends, with fallback on 404 to assist migration between them",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "types:check": "tsc --noEmit"
   },
   "repository": {
@@ -23,10 +23,10 @@
     "@aws-sdk/node-http-handler": "^3.374.0",
     "@aws-sdk/s3-request-presigner": "^3.994.0",
     "@google-cloud/storage": "^7.19.0",
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/stream-utils": "*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
+    "@overleaf/stream-utils": "workspace:*",
     "fast-crc32c": "overleaf/node-fast-crc32c#aae6b2a4c7a7a159395df9cc6c38dfde702d6f51",
     "glob": "^12.0.0",
     "range-parser": "^1.2.1",

+ 1 - 0
libraries/overleaf-editor-core/buildscript.txt

@@ -5,5 +5,6 @@ overleaf-editor-core
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=overleaf-editor-core
 --pipeline-owner=44
 --public-repo=False

+ 3 - 3
libraries/overleaf-editor-core/package.json

@@ -4,10 +4,10 @@
   "description": "Library shared between the editor server and clients.",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },
@@ -25,7 +25,7 @@
     "typescript": "^5.0.4"
   },
   "dependencies": {
-    "@overleaf/o-error": "*",
+    "@overleaf/o-error": "workspace:*",
     "check-types": "^5.1.0",
     "lodash": "^4.17.19",
     "p-map": "^4.0.0",

+ 1 - 0
libraries/promise-utils/buildscript.txt

@@ -5,5 +5,6 @@ promise-utils
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/promise-utils
 --pipeline-owner=32
 --public-repo=False

+ 2 - 2
libraries/promise-utils/package.json

@@ -4,11 +4,11 @@
   "description": "utilities to help working with promises",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "types:check": "tsc --noEmit"
   },
   "author": "Overleaf (https://www.overleaf.com)",

+ 1 - 0
libraries/ranges-tracker/buildscript.txt

@@ -5,5 +5,6 @@ ranges-tracker
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/ranges-tracker
 --pipeline-owner=44
 --public-repo=False

+ 2 - 2
libraries/ranges-tracker/package.json

@@ -11,8 +11,8 @@
   "scripts": {
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test": "npm run lint && npm run types:check && npm run test:unit",
-    "test:ci": "npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },

+ 1 - 0
libraries/redis-wrapper/buildscript.txt

@@ -5,5 +5,6 @@ redis-wrapper
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/redis-wrapper
 --pipeline-owner=32
 --public-repo=False

+ 7 - 7
libraries/redis-wrapper/package.json

@@ -15,23 +15,23 @@
   "scripts": {
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test": "npm run lint && npm run types:check && npm run test:unit",
-    "test:ci": "npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },
   "peerDependencies": {
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/o-error": "*"
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/o-error": "workspace:*"
   },
   "dependencies": {
     "async": "^3.2.5",
     "ioredis": "~4.27.1"
   },
   "devDependencies": {
-    "@overleaf/logger": "*",
-    "@overleaf/o-error": "*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
     "chai": "^4.3.6",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",

+ 1 - 0
libraries/settings/buildscript.txt

@@ -5,5 +5,6 @@ settings
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/settings
 --pipeline-owner=32
 --public-repo=False

+ 2 - 2
libraries/settings/package.json

@@ -7,8 +7,8 @@
   "scripts": {
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test": "npm run lint && npm run types:check && npm run test:unit",
-    "test:ci": "npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "types:check": "tsc --noEmit"
   },

+ 1 - 0
libraries/stream-utils/buildscript.txt

@@ -5,5 +5,6 @@ stream-utils
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/stream-utils
 --pipeline-owner=32
 --public-repo=False

+ 2 - 2
libraries/stream-utils/package.json

@@ -4,11 +4,11 @@
   "description": "stream handling utilities",
   "main": "index.js",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "types:check": "tsc --noEmit"
   },
   "author": "Overleaf (https://www.overleaf.com)",

+ 1 - 0
libraries/validation-tools/buildscript.txt

@@ -5,6 +5,7 @@ validation-tools
 --esmock-loader=False
 --is-library=True
 --node-version=24.14.1
+--package-name=@overleaf/validation-tools
 --public-repo=False
 --test-acceptance-vitest=True
 --test-unit-vitest=True

+ 3 - 3
libraries/validation-tools/package.json

@@ -10,15 +10,15 @@
   "license": "AGPL-3.0-only",
   "version": "1.0.0",
   "scripts": {
-    "test": "npm run lint && npm run types:check && npm run test:unit",
+    "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
-    "test:ci": "npm run test:unit",
+    "test:ci": "yarn run test:unit",
     "test:unit": "vitest --config vitest.config.ts",
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
-    "@overleaf/o-error": "*",
+    "@overleaf/o-error": "workspace:*",
     "mongodb": "^6.12.0",
     "zod": "^4.1.8",
     "zod-validation-error": "^4.0.1"

+ 186 - 26
package.json

@@ -1,9 +1,7 @@
 {
   "name": "overleaf",
   "private": true,
-  "dependencies": {
-    "patch-package": "^8.0.0"
-  },
+  "packageManager": "yarn@4.14.1",
   "devDependencies": {
     "@prettier/plugin-pug": "^3.4.0",
     "@types/chai": "^4.3.0",
@@ -30,29 +28,192 @@
     "typescript": "^5.9.3"
   },
   "engines": {
-    "npm": "11.11.0"
+    "node": ">=20.0.0"
   },
-  "overrides": {
-    "request@2.88.2": {
-      "tough-cookie": "5.1.2",
-      "form-data": "2.5.5",
-      "qs": "6.14.1"
-    },
-    "cypress@13.13.2": {
-      "@cypress/request@3.0.9": {
-        "qs": "6.14.1"
-      }
-    },
+  "resolutions": {
+    "sandboxed-module": "patch:sandboxed-module@npm%3A2.0.4#~/.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch",
+    "request/tough-cookie": "5.1.2",
+    "request/form-data": "2.5.5",
+    "request/qs": "6.14.1",
+    "@cypress/request/qs": "6.14.1",
     "@opentelemetry/api": "1.9.0",
-    "mocha@^11.1.0": {
-      "serialize-javascript": "7.0.5"
-    },
-    "pprof": {
-      "protobufjs": "7.5.5"
-    },
-    "@google-cloud/profiler": {
-      "protobufjs": "7.5.5"
-    }
+    "mocha/serialize-javascript": "7.0.5",
+    "pprof/protobufjs": "7.5.5",
+    "@google-cloud/profiler/protobufjs": "7.5.5",
+    "mocha-multi-reporters": "patch:mocha-multi-reporters@npm%3A1.5.1#~/.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch",
+    "pdfjs-dist": "patch:pdfjs-dist@npm%3A5.1.91#~/.yarn/patches/pdfjs-dist-npm-5.1.91.patch",
+    "referer-parser": "patch:referer-parser@npm%3A0.0.3#~/.yarn/patches/referer-parser-npm-0.0.3.patch",
+    "sass": "1.77.1",
+    "@codemirror/autocomplete": "patch:@codemirror/autocomplete@npm%3A6.18.4#~/.yarn/patches/@codemirror-autocomplete-npm-6.18.4.patch",
+    "@codemirror/commands": "6.10.1",
+    "@codemirror/language": "6.12.1",
+    "@codemirror/lint": "6.9.2",
+    "@codemirror/search": "patch:@codemirror/search@npm%3A6.5.8#~/.yarn/patches/@codemirror-search-npm-6.5.8.patch",
+    "@codemirror/state": "6.5.4",
+    "@codemirror/view": "6.38.6",
+    "@lezer/common": "1.5.0",
+    "@lezer/highlight": "1.2.3",
+    "@lezer/lr": "1.4.7",
+    "@types/react": "18.3.28",
+    "@types/react-dom": "18.3.7",
+    "cheerio": "1.0.0-rc.10",
+    "react": "18.3.1",
+    "react-dom": "18.3.1",
+    "sinon-chai": "3.7.0",
+    "vitest": "4.0.18",
+    "@vitest/coverage-istanbul": "4.0.18",
+    "@vitest/expect": "4.0.18",
+    "@vitest/mocker": "4.0.18",
+    "@vitest/pretty-format": "4.0.18",
+    "@vitest/runner": "4.0.18",
+    "@vitest/snapshot": "4.0.18",
+    "@vitest/spy": "4.0.18",
+    "@vitest/utils": "4.0.18",
+    "i18next-scanner/i18next": "23.16.8",
+    "downshift": "9.0.9",
+    "body-parser@npm:1.20.4": "patch:body-parser@npm%3A1.20.4#~/.yarn/patches/body-parser-npm-1.20.4.patch",
+    "cypress-multi-reporters": "patch:cypress-multi-reporters@npm%3A2.0.5#~/.yarn/patches/cypress-multi-reporters-npm-2.0.5.patch",
+    "forwarded@npm:0.2.0": "patch:forwarded@npm%3A0.2.0#~/.yarn/patches/forwarded-npm-0.2.0.patch",
+    "multer@npm:2.1.1": "patch:multer@npm%3A2.1.1#~/.yarn/patches/multer-npm-2.1.1.patch",
+    "node-fetch": "patch:node-fetch@npm%3A2.7.0#~/.yarn/patches/node-fetch-npm-2.7.0.patch",
+    "passport-oauth2": "patch:passport-oauth2@npm%3A1.6.1#~/.yarn/patches/passport-oauth2-npm-1.6.1.patch",
+    "send": "patch:send@npm%3A0.19.0#~/.yarn/patches/send-npm-0.19.0.patch",
+    "serve-static": "1.16.2",
+    "@uppy/xhr-upload": "3.6.0",
+    "recurly": "4.12.0",
+    "mongoose": "8.9.5",
+    "pg": "8.7.1",
+    "pg-query-stream": "4.7.1",
+    "@aws-sdk/client-s3": "3.994.0",
+    "@aws-sdk/client-ses": "3.994.0",
+    "@aws-sdk/s3-request-presigner": "3.994.0",
+    "contentful": "10.8.5",
+    "@contentful/rich-text-html-renderer": "16.0.2",
+    "@contentful/rich-text-types": "16.0.2",
+    "i18next": "23.10.0",
+    "sanitize-html": "2.12.1",
+    "lodash": "4.17.23",
+    "express-session": "1.17.2",
+    "ioredis": "4.27.11",
+    "webpack": "5.98.0",
+    "knip": "5.64.1",
+    "eslint-plugin-testing-library": "7.5.3",
+    "chart.js": "4.0.1",
+    "mock-fs": "5.2.0",
+    "@customerio/cdp-analytics-node": "0.3.9",
+    "@google-cloud/bigquery": "8.1.1",
+    "moment": "2.29.4",
+    "sequelize-cli": "6.6.0",
+    "async": "3.2.5",
+    "dockerode": "4.0.9",
+    "tar-fs": "3.1.1",
+    "cluster-key-slot": "1.1.0",
+    "@octokit/request": "9.2.2",
+    "randomstring": "1.2.2",
+    "vite": "7.3.1",
+    "isomorphic-git": "1.33.1",
+    "http-status": "1.5.0",
+    "knex": "2.4.0",
+    "utf-8-validate": "5.0.8",
+    "samlp": "7.0.2",
+    "compression": "1.7.4",
+    "cookie-parser": "1.4.6",
+    "react-cookie": "7.2.0",
+    "react-dropzone": "14.2.3",
+    "@babel/core": "7.28.5",
+    "@babel/preset-env": "7.28.5",
+    "@babel/register": "7.28.3",
+    "@testing-library/react": "16.3.0",
+    "@vitejs/plugin-react": "4.4.1",
+    "babel-loader": "10.0.0",
+    "css-loader": "6.8.1",
+    "cssnano": "7.1.4",
+    "mini-css-extract-plugin": "2.7.6",
+    "nodemon": "3.0.1",
+    "postcss": "8.5.8",
+    "postcss-loader": "7.3.3",
+    "postcss-reporter": "7.0.5",
+    "style-loader": "3.3.3",
+    "webpack-hot-middleware": "2.25.3",
+    "webpack-manifest-plugin": "5.0.0",
+    "zod": "4.1.11",
+    "zod-validation-error": "4.0.1",
+    "simple-oauth2": "5.0.0",
+    "@types/simple-oauth2": "5.0.7",
+    "@ai-sdk/mcp": "1.0.25",
+    "@ai-sdk/openai": "3.0.41",
+    "@node-oauth/oauth2-server": "5.3.0",
+    "@phosphor-icons/react": "2.1.7",
+    "@slack/webhook": "7.0.2",
+    "@stripe/react-stripe-js": "3.9.0",
+    "@stripe/stripe-js": "7.7.0",
+    "ai": "6.0.116",
+    "cache-flow": "1.9.0",
+    "focus-trap-react": "11.0.4",
+    "i18next-http-middleware": "3.5.0",
+    "jose": "4.15.5",
+    "nodemailer": "7.0.11",
+    "on-headers": "1.0.2",
+    "pug": "3.0.3",
+    "rate-limiter-flexible": "2.4.1",
+    "react-hook-form": "7.71.1",
+    "stripe": "18.4.0",
+    "@ai-sdk/react": "3.0.118",
+    "@babel/plugin-proposal-decorators": "7.28.0",
+    "@floating-ui/react": "0.27.16",
+    "@juggle/resize-observer": "3.3.1",
+    "@storybook/addon-a11y": "10.3.5",
+    "@storybook/addon-essentials": "10.3.5",
+    "@storybook/addon-interactions": "10.3.5",
+    "@storybook/addon-links": "10.3.5",
+    "@storybook/cli": "10.3.5",
+    "@storybook/react": "10.3.5",
+    "@storybook/react-webpack5": "10.3.5",
+    "@storybook/theming": "10.3.5",
+    "@streamdown/cjk": "1.0.2",
+    "@testing-library/dom": "10.4.0",
+    "@testing-library/user-event": "14.5.2",
+    "@types/express": "4.17.23",
+    "@types/recurly__recurly-js": "4.38.0",
+    "@types/sanitize-html": "2.16.0",
+    "@uppy/dashboard": "3.7.1",
+    "@uppy/drag-drop": "3.0.3",
+    "@uppy/file-input": "3.0.4",
+    "@uppy/progress-bar": "3.0.4",
+    "@uppy/react": "3.2.1",
+    "autoprefixer": "10.4.16",
+    "babel-plugin-module-resolver": "5.0.2",
+    "backbone": "1.6.0",
+    "dompurify": "3.3.3",
+    "eventsource-client": "1.1.4",
+    "fake-indexeddb": "6.0.0",
+    "formik": "2.2.9",
+    "handlebars": "4.7.8",
+    "html-webpack-plugin": "5.5.3",
+    "katex": "0.16.28",
+    "match-sorter": "6.3.1",
+    "micromark": "4.0.0",
+    "pirates": "4.0.6",
+    "qrcode": "1.5.0",
+    "react-chartjs-2": "5.0.1",
+    "react-i18next": "13.3.1",
+    "react-resizable-panels": "2.1.1",
+    "rehype-harden": "1.1.7",
+    "scroll-into-view-if-needed": "2.2.28",
+    "storybook": "10.3.5",
+    "streamdown": "2.2.0",
+    "tailwindcss": "3.4.17",
+    "terser-webpack-plugin": "5.3.17",
+    "thread-loader": "patch:thread-loader@npm%3A4.0.2#~/.yarn/patches/thread-loader-npm-4.0.2-dab5735f54.patch",
+    "ts-loader": "9.5.4",
+    "unist-util-visit": "5.0.0",
+    "use-stick-to-bottom": "1.1.1",
+    "webpack-dev-server": "5.2.2",
+    "zustand": "5.0.8",
+    "retry-request@npm:^8.0.0": "patch:retry-request@npm%3A8.0.2#~/.yarn/patches/retry-request-npm-8.0.2-448ad084c8.patch",
+    "retry-request@npm:^7.0.0": "patch:retry-request@npm%3A7.0.2#~/.yarn/patches/retry-request-npm-7.0.2-a41087680c.patch",
+    "teeny-request@npm:^10.0.0": "patch:teeny-request@npm%3A10.1.0#~/.yarn/patches/teeny-request-npm-10.1.0.patch",
+    "teeny-request@npm:^9.0.0": "patch:teeny-request@npm%3A9.0.0#~/.yarn/patches/teeny-request-npm-9.0.0-4d571e3c55.patch"
   },
   "scripts": {
     "format": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check",
@@ -66,8 +227,7 @@
     "format:monorepo-check": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml'",
     "format:monorepo-check:fix": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --write --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml'",
     "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
-    "lint:fix": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --fix .",
-    "postinstall": "patch-package"
+    "lint:fix": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --fix ."
   },
   "workspaces": [
     "jobs/mirror-documentation",

+ 14 - 9
server-ce/Dockerfile

@@ -8,25 +8,30 @@ FROM $OVERLEAF_BASE_TAG
 
 WORKDIR /overleaf
 
-# Add required source files for npm install
-# -----------------------------------------
-COPY --parents libraries/*/package.json patches/ services/*/package.json tools/migrations/ package.json package-lock.json /overleaf/
+# Add required source files for yarn install
+# -------------------------------------------
+COPY --parents libraries/*/package.json .yarn/patches/ services/*/package.json tools/migrations/ package.json yarn.lock .yarnrc.yml /overleaf/
 COPY server-ce/genScript.js server-ce/services.js /overleaf/
 
-# Install npm dependencies
-# ------------------------
+# Pre-install yarn via corepack so it is available at runtime for all users
+# -------------------------------------------------------------------------
+ENV COREPACK_HOME=/opt/corepack
+RUN corepack install
+
+# Install yarn dependencies
+# -------------------------
 RUN --mount=type=cache,target=/root/.cache \
-    --mount=type=cache,target=/root/.npm \
-    --mount=type=cache,target=/overleaf/services/web/node_modules/.cache,id=server-ce-webpack-cache \
+    --mount=type=cache,target=/root/.yarn/berry/cache,id=server-ce-yarn-cache \
     --mount=type=tmpfs,target=/tmp node genScript install | bash
 
 # Add the actual source files
 # ---------------------------
 COPY --parents libraries/ services/ tools/migrations/ /overleaf/
 RUN --mount=type=cache,target=/root/.cache \
-    --mount=type=cache,target=/root/.npm \
+    --mount=type=cache,target=/root/.yarn/berry/cache,id=server-ce-yarn-cache \
     --mount=type=cache,target=/overleaf/services/web/node_modules/.cache,id=server-ce-webpack-cache \
-    --mount=type=tmpfs,target=/tmp node genScript compile | bash
+    --mount=type=tmpfs,target=/tmp \
+    YARN_CACHE_FOLDER=/tmp/.yarn-cache node genScript compile | bash
 
 # Copy runit service startup scripts to its location
 # --------------------------------------------------

+ 1 - 0
server-ce/Dockerfile-base

@@ -33,6 +33,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
 &&  echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
 &&  apt-get update \
 &&  apt-get install -y nodejs \
+&&  corepack enable \
     \
 &&  rm -rf \
 # We are adding a custom nginx config in the main Dockerfile.

+ 5 - 5
server-ce/genScript.js

@@ -5,7 +5,7 @@ console.log('set -ex')
 
 switch (process.argv.pop()) {
   case 'install':
-    console.log('npm install --omit=dev')
+    console.log('yarn workspaces focus --all --production')
     break
   case 'compile':
     for (const service of services) {
@@ -13,18 +13,18 @@ switch (process.argv.pop()) {
       switch (service.name) {
         case 'web':
           // precompile pug in background
-          console.log('npm run precompile-pug &')
+          console.log('yarn run precompile-pug &')
           console.log('pug_precompile=$!')
 
           // Avoid downloading of cypress
           console.log('export CYPRESS_INSTALL_BINARY=0')
 
           // install webpack and frontend dependencies
-          console.log('npm install --include=dev')
+          console.log('yarn install')
           // run webpack
-          console.log('npm run webpack:production')
+          console.log('yarn run webpack:production')
           // uninstall webpack and frontend dependencies
-          console.log('npm prune --omit=dev')
+          console.log('yarn workspaces focus --all --production')
 
           // Wait for pug precompile to finish
           console.log('wait "$pug_precompile"')

+ 1 - 1
server-ce/init_scripts/900_run_web_migrations.sh

@@ -9,5 +9,5 @@ fi
 
 echo "Running migrations for $environment"
 cd /overleaf/tools/migrations
-/sbin/setuser www-data npm run migrations -- migrate -t "$environment"
+/sbin/setuser www-data yarn run migrations migrate -t "$environment"
 echo "Finished migrations"

+ 1 - 1
server-ce/test/Makefile

@@ -35,7 +35,7 @@ test-e2e-native:
 	CYPRESS_HOST_ADMIN_URL='http://127.0.0.1:3232' \
 	CYPRESS_SAML_URL='http://127.0.0.1:3233' \
 	CYPRESS_MAILTRAP_URL='http://127.0.0.1:3234' \
-		npm run cypress:open
+		yarn run cypress:open
 
 # For testing "native" on Linux
 test-e2e-native-linux:

+ 1 - 1
server-ce/test/docker-compose.yml

@@ -72,7 +72,7 @@ services:
         - USER_UID
         - USER_GID
     stop_grace_period: 0s
-    entrypoint: npm
+    entrypoint: yarn
     command: run cypress:run
     # See comment in Makefile regarding matching file paths
     working_dir: $PWD

+ 2 - 2
server-ce/test/package.json

@@ -11,8 +11,8 @@
   },
   "devDependencies": {
     "@isomorphic-git/lightning-fs": "^4.6.0",
-    "@overleaf/promise-utils": "*",
-    "@overleaf/validation-tools": "*",
+    "@overleaf/promise-utils": "workspace:*",
+    "@overleaf/validation-tools": "workspace:*",
     "@testing-library/cypress": "10.1.0",
     "@types/adm-zip": "^0.5.7",
     "@types/uuid": "^9.0.8",

+ 5 - 3
services/chat/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/chat
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 
 # Google Cloud Storage needs a writable $HOME/.config for resumable uploads
 # (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
@@ -12,7 +14,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -22,10 +24,10 @@ COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/pack
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY services/chat/package.json /overleaf/services/chat/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/chat overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/chat/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -25,7 +25,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/services/chat/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -52,11 +52,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -95,19 +95,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/chat
+	$(RUN_LINTING_MONOREPO) format services/chat
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/chat
+	$(RUN_LINTING_CI_MONOREPO) format services/chat
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/chat
+	$(RUN_LINTING_MONOREPO) format:fix services/chat
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -145,7 +145,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -158,7 +158,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 1 - 0
services/chat/buildscript.txt

@@ -4,5 +4,6 @@ chat
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=@overleaf/chat
 --pipeline-owner=44
 --public-repo=False

+ 2 - 2
services/chat/docker-compose.ci.yml

@@ -10,7 +10,7 @@ services:
       - ./reports:/overleaf/services/chat/reports
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -39,7 +39,7 @@ services:
         condition: service_started
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 8 - 2
services/chat/docker-compose.yml

@@ -9,6 +9,9 @@ services:
       - .:/overleaf/services/chat
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/chat
@@ -19,7 +22,7 @@ services:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
     depends_on:
       mongo:
@@ -31,6 +34,9 @@ services:
       - .:/overleaf/services/chat
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/chat
@@ -47,7 +53,7 @@ services:
       mongo:
         condition: service_started
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   mongo:
     image: mongo:8.0.11

+ 3 - 3
services/chat/package.json

@@ -6,8 +6,8 @@
   "type": "module",
   "scripts": {
     "start": "node app.js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:unit": "yarn run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "test:acceptance:_run": "mocha --recursive --timeout 15000 --exit --retries=$RETRIES $@ test/acceptance/js",
     "test:unit:_run": "mocha --recursive --exit $@ test/unit/js",
@@ -28,7 +28,7 @@
     "mongodb": "6.12.0"
   },
   "devDependencies": {
-    "@overleaf/migrations": "*",
+    "@overleaf/migrations": "workspace:*",
     "acorn": "^7.1.1",
     "ajv": "^6.12.0",
     "chai": "^4.3.6",

+ 5 - 3
services/clsi/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/clsi
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 COPY services/clsi/install_deps.sh /overleaf/services/clsi/
 RUN chmod 0755 ./install_deps.sh && ./install_deps.sh
 ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
@@ -16,7 +18,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -26,9 +28,9 @@ COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/pack
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
 COPY services/clsi/package.json /overleaf/services/clsi/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/clsi overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/clsi/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -25,7 +25,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/libraries/stream-utils/package.json \
 	$(MONOREPO)/services/clsi/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -53,11 +53,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -96,19 +96,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/clsi
+	$(RUN_LINTING_MONOREPO) format services/clsi
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/clsi
+	$(RUN_LINTING_CI_MONOREPO) format services/clsi
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/clsi
+	$(RUN_LINTING_MONOREPO) format:fix services/clsi
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -146,7 +146,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -161,7 +161,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 2 - 1
services/clsi/buildscript.txt

@@ -1,10 +1,11 @@
 clsi
 --data-dirs=cache,compiles,output
 --dependencies=
---env-add=DOWNLOAD_HOST=http://clsi-nginx:8080,ALLOWED_COMPILE_GROUPS="clsi-perf simple-latex-file",ENABLE_PDF_CACHING="true",PDF_CACHING_ENABLE_WORKER_POOL="true",ALLOWED_IMAGES="quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9",TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_IMAGE_NAME_OVERRIDE=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER="tex",SANDBOXED_COMPILES="true",SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output,ENABLE_PANDOC_CONVERSIONS=true
+--env-add=DOWNLOAD_HOST=http://clsi-nginx:8080,ALLOWED_COMPILE_GROUPS=clsi-perf simple-latex-file,ENABLE_PDF_CACHING=true,PDF_CACHING_ENABLE_WORKER_POOL=true,ALLOWED_IMAGES=quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9,TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_IMAGE_NAME_OVERRIDE=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER=tex,SANDBOXED_COMPILES=true,SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output,ENABLE_PANDOC_CONVERSIONS=true
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=@overleaf/clsi
 --pipeline-owner=32
 --public-repo=True
 --test-unit-vitest=True

+ 8 - 8
services/clsi/docker-compose.ci.yml

@@ -8,7 +8,7 @@ services:
     volumes:
       - ./reports:/overleaf/services/clsi/reports
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -28,14 +28,14 @@ services:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
       DOWNLOAD_HOST: http://clsi-nginx:8080
-      ALLOWED_COMPILE_GROUPS: "clsi-perf simple-latex-file"
-      ENABLE_PDF_CACHING: "true"
-      PDF_CACHING_ENABLE_WORKER_POOL: "true"
-      ALLOWED_IMAGES: "quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9"
+      ALLOWED_COMPILE_GROUPS: clsi-perf simple-latex-file
+      ENABLE_PDF_CACHING: true
+      PDF_CACHING_ENABLE_WORKER_POOL: true
+      ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9
       TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1
       TEX_LIVE_IMAGE_NAME_OVERRIDE: us-east1-docker.pkg.dev/overleaf-ops/ol-docker
-      TEXLIVE_IMAGE_USER: "tex"
-      SANDBOXED_COMPILES: "true"
+      TEXLIVE_IMAGE_USER: tex
+      SANDBOXED_COMPILES: true
       SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles
       SANDBOXED_COMPILES_HOST_DIR_OUTPUT: $PWD/output
       ENABLE_PANDOC_CONVERSIONS: true
@@ -47,7 +47,7 @@ services:
     depends_on:
       clsi-nginx:
         condition: service_started
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 14 - 8
services/clsi/docker-compose.yml

@@ -12,6 +12,9 @@ services:
       - .:/overleaf/services/clsi
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     working_dir: /overleaf/services/clsi
     environment:
@@ -20,7 +23,7 @@ services:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
 
   test_acceptance:
     build:
@@ -31,6 +34,9 @@ services:
       - .:/overleaf/services/clsi
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - /var/run/docker.sock:/var/run/docker.sock
     working_dir: /overleaf/services/clsi
     environment:
@@ -42,21 +48,21 @@ services:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
       DOWNLOAD_HOST: http://clsi-nginx:8080
-      ALLOWED_COMPILE_GROUPS: "clsi-perf simple-latex-file"
-      ENABLE_PDF_CACHING: "true"
-      PDF_CACHING_ENABLE_WORKER_POOL: "true"
-      ALLOWED_IMAGES: "quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9"
+      ALLOWED_COMPILE_GROUPS: clsi-perf simple-latex-file
+      ENABLE_PDF_CACHING: true
+      PDF_CACHING_ENABLE_WORKER_POOL: true
+      ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9
       TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1
       TEX_LIVE_IMAGE_NAME_OVERRIDE: us-east1-docker.pkg.dev/overleaf-ops/ol-docker
-      TEXLIVE_IMAGE_USER: "tex"
-      SANDBOXED_COMPILES: "true"
+      TEXLIVE_IMAGE_USER: tex
+      SANDBOXED_COMPILES: true
       SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles
       SANDBOXED_COMPILES_HOST_DIR_OUTPUT: $PWD/output
       ENABLE_PANDOC_CONVERSIONS: true
     depends_on:
       clsi-nginx:
         condition: service_started
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   clsi-nginx:
     image: nginx:1.28

+ 10 - 10
services/clsi/package.json

@@ -7,22 +7,22 @@
   "scripts": {
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --timeout 15000 --exit --retries=$RETRIES $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
     "test:unit:_run": "vitest --config ./vitest.config.unit.cjs",
-    "test:unit": "npm run test:unit:_run",
+    "test:unit": "yarn run test:unit:_run",
     "nodemon": "node --watch app.js",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix .",
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
-    "@overleaf/fetch-utils": "*",
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/promise-utils": "*",
-    "@overleaf/settings": "*",
-    "@overleaf/stream-utils": "*",
+    "@overleaf/fetch-utils": "workspace:*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
+    "@overleaf/promise-utils": "workspace:*",
+    "@overleaf/settings": "workspace:*",
+    "@overleaf/stream-utils": "workspace:*",
     "archiver": "5.3.2",
     "async": "^3.2.5",
     "body-parser": "1.20.4",
@@ -31,7 +31,7 @@
     "express": "4.22.1",
     "lodash": "^4.17.21",
     "multer": "2.1.1",
-    "overleaf-editor-core": "*",
+    "overleaf-editor-core": "workspace:*",
     "p-limit": "^3.1.0",
     "request": "2.88.2",
     "send": "^0.19.0",

+ 26 - 0
services/clsi/test/unit/setup.js

@@ -4,7 +4,33 @@ import chaiAsPromised from 'chai-as-promised'
 
 // Setup chai
 chai.should()
+
+// Workaround: vitest's built-in chai plugins register spy-related properties
+// (e.g. callCount) as getter-only via addChainableMethod. sinon-chai then tries
+// to overwrite them via addMethod (plain property assignment), which fails.
+// Pre-delete the conflicting getter-only properties so the assignment succeeds.
+const sinonChaiMethodProps = [
+  'callCount',
+  'calledBefore',
+  'calledAfter',
+  'calledImmediatelyBefore',
+  'calledImmediatelyAfter',
+  'calledOn',
+  'calledWith',
+  'calledOnceWith',
+  'calledWithExactly',
+  'calledOnceWithExactly',
+  'calledWithMatch',
+  'returned',
+  'thrown',
+]
+for (const name of sinonChaiMethodProps) {
+  if (Object.getOwnPropertyDescriptor(chai.Assertion.prototype, name)?.get) {
+    delete chai.Assertion.prototype[name]
+  }
+}
 chai.use(sinonChai)
+
 chai.use(chaiAsPromised)
 
 beforeEach(() => {

+ 5 - 3
services/contacts/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/contacts
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 
 # Google Cloud Storage needs a writable $HOME/.config for resumable uploads
 # (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
@@ -12,7 +14,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -22,10 +24,10 @@ COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/pack
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY services/contacts/package.json /overleaf/services/contacts/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/contacts overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/contacts/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -25,7 +25,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/services/contacts/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -52,11 +52,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/contacts/reports:/overleaf/services/contacts/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/contacts/reports:/overleaf/services/contacts/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/contacts/reports:/overleaf/services/contacts/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/contacts/reports:/overleaf/services/contacts/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -95,19 +95,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/contacts
+	$(RUN_LINTING_MONOREPO) format services/contacts
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/contacts
+	$(RUN_LINTING_CI_MONOREPO) format services/contacts
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/contacts
+	$(RUN_LINTING_MONOREPO) format:fix services/contacts
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -145,7 +145,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -158,7 +158,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 1 - 0
services/contacts/buildscript.txt

@@ -4,5 +4,6 @@ contacts
 --env-pass-through=
 --esmock-loader=True
 --node-version=24.14.1
+--package-name=@overleaf/contacts
 --pipeline-owner=52
 --public-repo=False

+ 2 - 2
services/contacts/docker-compose.ci.yml

@@ -10,7 +10,7 @@ services:
       - ./reports:/overleaf/services/contacts/reports
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -39,7 +39,7 @@ services:
         condition: service_started
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 8 - 2
services/contacts/docker-compose.yml

@@ -9,6 +9,9 @@ services:
       - .:/overleaf/services/contacts
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/contacts
@@ -19,7 +22,7 @@ services:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
     depends_on:
       mongo:
@@ -31,6 +34,9 @@ services:
       - .:/overleaf/services/contacts
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/contacts
@@ -47,7 +53,7 @@ services:
       mongo:
         condition: service_started
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   mongo:
     image: mongo:8.0.11

+ 7 - 7
services/contacts/package.json

@@ -7,19 +7,19 @@
   "scripts": {
     "start": "node app.js",
     "test:acceptance:_run": "mocha --loader=esmock --recursive --timeout 15000 --exit --retries=$RETRIES $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
     "test:unit:_run": "mocha --loader=esmock --recursive --exit $@ test/unit/js",
-    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
+    "test:unit": "yarn run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix .",
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/mongo-utils": "*",
-    "@overleaf/settings": "*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/mongo-utils": "workspace:*",
+    "@overleaf/settings": "workspace:*",
     "async": "^3.2.5",
     "body-parser": "1.20.4",
     "bunyan": "^1.8.15",
@@ -29,7 +29,7 @@
     "underscore": "~1.13.1"
   },
   "devDependencies": {
-    "@overleaf/migrations": "*",
+    "@overleaf/migrations": "workspace:*",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "esmock": "^2.6.3",

+ 5 - 3
services/docstore/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/docstore
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 
 # Google Cloud Storage needs a writable $HOME/.config for resumable uploads
 # (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
@@ -12,7 +14,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -24,10 +26,10 @@ COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
 COPY services/docstore/package.json /overleaf/services/docstore/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/docstore overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/docstore/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -27,7 +27,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/stream-utils/package.json \
 	$(MONOREPO)/services/docstore/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -54,11 +54,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -97,19 +97,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/docstore
+	$(RUN_LINTING_MONOREPO) format services/docstore
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/docstore
+	$(RUN_LINTING_CI_MONOREPO) format services/docstore
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/docstore
+	$(RUN_LINTING_MONOREPO) format:fix services/docstore
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -147,7 +147,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -160,7 +160,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 1 - 0
services/docstore/buildscript.txt

@@ -4,6 +4,7 @@ docstore
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=@overleaf/docstore
 --pipeline-owner=32
 --public-repo=True
 --test-unit-vitest=True

+ 2 - 2
services/docstore/docker-compose.ci.yml

@@ -11,7 +11,7 @@ services:
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -46,7 +46,7 @@ services:
         condition: service_healthy
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 8 - 2
services/docstore/docker-compose.yml

@@ -9,6 +9,9 @@ services:
       - .:/overleaf/services/docstore
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
@@ -20,7 +23,7 @@ services:
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
     depends_on:
       mongo:
@@ -32,6 +35,9 @@ services:
       - .:/overleaf/services/docstore
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/docstore
@@ -53,7 +59,7 @@ services:
       gcs:
         condition: service_healthy
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   mongo:
     image: mongo:8.0.11

+ 11 - 11
services/docstore/package.json

@@ -7,23 +7,23 @@
   "scripts": {
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --timeout 15000 --exit --retries=$RETRIES $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
     "test:unit:_run": "vitest --config ./vitest.config.unit.cjs",
-    "test:unit": "npm run test:unit:_run",
+    "test:unit": "yarn run test:unit:_run",
     "nodemon": "node --watch app.js",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix .",
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
-    "@overleaf/fetch-utils": "*",
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/mongo-utils": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/object-persistor": "*",
-    "@overleaf/promise-utils": "*",
-    "@overleaf/settings": "*",
+    "@overleaf/fetch-utils": "workspace:*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/mongo-utils": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
+    "@overleaf/object-persistor": "workspace:*",
+    "@overleaf/promise-utils": "workspace:*",
+    "@overleaf/settings": "workspace:*",
     "@overleaf/stream-utils": "^0.1.0",
     "async": "^3.2.5",
     "body-parser": "1.20.4",
@@ -36,7 +36,7 @@
   },
   "devDependencies": {
     "@google-cloud/storage": "^7.19.0",
-    "@overleaf/migrations": "*",
+    "@overleaf/migrations": "workspace:*",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "mocha": "^11.1.0",

+ 5 - 3
services/document-updater/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/document-updater
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 
 # Google Cloud Storage needs a writable $HOME/.config for resumable uploads
 # (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
@@ -12,7 +14,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -25,10 +27,10 @@ COPY libraries/redis-wrapper/package.json /overleaf/libraries/redis-wrapper/pack
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY services/document-updater/package.json /overleaf/services/document-updater/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/document-updater overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/document-updater/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -28,7 +28,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/services/document-updater/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -55,11 +55,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -98,19 +98,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/document-updater
+	$(RUN_LINTING_MONOREPO) format services/document-updater
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/document-updater
+	$(RUN_LINTING_CI_MONOREPO) format services/document-updater
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/document-updater
+	$(RUN_LINTING_MONOREPO) format:fix services/document-updater
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -148,7 +148,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -161,7 +161,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 1 - 0
services/document-updater/buildscript.txt

@@ -4,5 +4,6 @@ document-updater
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=@overleaf/document-updater
 --pipeline-owner=44
 --public-repo=True

+ 2 - 2
services/document-updater/docker-compose.ci.yml

@@ -10,7 +10,7 @@ services:
       - ./reports:/overleaf/services/document-updater/reports
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -53,7 +53,7 @@ services:
         condition: service_healthy
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 8 - 2
services/document-updater/docker-compose.yml

@@ -9,6 +9,9 @@ services:
       - .:/overleaf/services/document-updater
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/document-updater
@@ -24,7 +27,7 @@ services:
       ANALYTICS_QUEUES_REDIS_HOST: redis_test
       ANALYTICS_QUEUES_BQ_SINK_REDIS_HOST: redis_test
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
     depends_on:
       mongo:
@@ -38,6 +41,9 @@ services:
       - .:/overleaf/services/document-updater
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/document-updater
@@ -61,7 +67,7 @@ services:
       redis_test:
         condition: service_healthy
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   redis_test:
     image: redis:7.4.8

+ 13 - 13
services/document-updater/package.json

@@ -6,9 +6,9 @@
   "scripts": {
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --timeout 15000 --exit --retries=$RETRIES $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
     "test:unit:_run": "mocha --recursive --exit $@ test/unit/js",
-    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
+    "test:unit": "yarn run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "benchmark:apply": "node benchmarks/apply",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
@@ -16,15 +16,15 @@
     "types:check": "tsc --noEmit"
   },
   "dependencies": {
-    "@overleaf/fetch-utils": "*",
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/mongo-utils": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/promise-utils": "*",
-    "@overleaf/ranges-tracker": "*",
-    "@overleaf/redis-wrapper": "*",
-    "@overleaf/settings": "*",
+    "@overleaf/fetch-utils": "workspace:*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/mongo-utils": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
+    "@overleaf/promise-utils": "workspace:*",
+    "@overleaf/ranges-tracker": "workspace:*",
+    "@overleaf/redis-wrapper": "workspace:*",
+    "@overleaf/settings": "workspace:*",
     "@types/chai-as-promised": "^7.1.8",
     "async": "^3.2.5",
     "body-parser": "1.20.4",
@@ -35,10 +35,10 @@
     "lodash": "^4.17.21",
     "minimist": "^1.2.8",
     "mongodb-legacy": "6.1.3",
-    "overleaf-editor-core": "*"
+    "overleaf-editor-core": "workspace:*"
   },
   "devDependencies": {
-    "@overleaf/migrations": "*",
+    "@overleaf/migrations": "workspace:*",
     "basic-auth": "^2.0.1",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",

+ 5 - 3
services/filestore/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/filestore
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 COPY services/filestore/install_deps.sh /overleaf/services/filestore/
 RUN chmod 0755 ./install_deps.sh && ./install_deps.sh
 
@@ -14,7 +16,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -23,9 +25,9 @@ COPY libraries/object-persistor/package.json /overleaf/libraries/object-persisto
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
 COPY services/filestore/package.json /overleaf/services/filestore/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus @overleaf/filestore overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/filestore/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -24,7 +24,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/libraries/stream-utils/package.json \
 	$(MONOREPO)/services/filestore/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -52,11 +52,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -95,19 +95,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/filestore
+	$(RUN_LINTING_MONOREPO) format services/filestore
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/filestore
+	$(RUN_LINTING_CI_MONOREPO) format services/filestore
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/filestore
+	$(RUN_LINTING_MONOREPO) format:fix services/filestore
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -145,7 +145,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -158,7 +158,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 2 - 1
services/filestore/buildscript.txt

@@ -1,10 +1,11 @@
 filestore
 --data-dirs=uploads,template_files
 --dependencies=s3,gcs
---env-add=ENABLE_CONVERSIONS="true",USE_PROM_METRICS="true",AWS_S3_USER_FILES_STORAGE_CLASS=REDUCED_REDUNDANCY,AWS_S3_USER_FILES_BUCKET_NAME=fake-user-files,AWS_S3_USER_FILES_DEK_BUCKET_NAME=fake-user-files-dek,AWS_S3_TEMPLATE_FILES_BUCKET_NAME=fake-template-files,GCS_USER_FILES_BUCKET_NAME=fake-gcs-user-files,GCS_TEMPLATE_FILES_BUCKET_NAME=fake-gcs-template-files
+--env-add=ENABLE_CONVERSIONS=true,USE_PROM_METRICS=true,AWS_S3_USER_FILES_STORAGE_CLASS=REDUCED_REDUNDANCY,AWS_S3_USER_FILES_BUCKET_NAME=fake-user-files,AWS_S3_USER_FILES_DEK_BUCKET_NAME=fake-user-files-dek,AWS_S3_TEMPLATE_FILES_BUCKET_NAME=fake-template-files,GCS_USER_FILES_BUCKET_NAME=fake-gcs-user-files,GCS_TEMPLATE_FILES_BUCKET_NAME=fake-gcs-template-files
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=@overleaf/filestore
 --pipeline-owner=32
 --public-repo=True
 --test-acceptance-shards=SHARD_01_,SHARD_02_,SHARD_03_

+ 4 - 4
services/filestore/docker-compose.ci.yml

@@ -12,7 +12,7 @@ services:
     volumes:
       - ./reports:/overleaf/services/filestore/reports
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -41,8 +41,8 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
-      ENABLE_CONVERSIONS: "true"
-      USE_PROM_METRICS: "true"
+      ENABLE_CONVERSIONS: true
+      USE_PROM_METRICS: true
       AWS_S3_USER_FILES_STORAGE_CLASS: REDUCED_REDUNDANCY
       AWS_S3_USER_FILES_BUCKET_NAME: fake-user-files
       AWS_S3_USER_FILES_DEK_BUCKET_NAME: fake-user-files-dek
@@ -62,7 +62,7 @@ services:
       gcs:
         condition: service_healthy
     user: node
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 10 - 4
services/filestore/docker-compose.yml

@@ -15,6 +15,9 @@ services:
       - .:/overleaf/services/filestore
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     working_dir: /overleaf/services/filestore
     environment:
@@ -23,7 +26,7 @@ services:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
 
   test_acceptance:
@@ -35,6 +38,9 @@ services:
       - .:/overleaf/services/filestore
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - minio-certs:/certs
     working_dir: /overleaf/services/filestore
     environment:
@@ -55,8 +61,8 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
-      ENABLE_CONVERSIONS: "true"
-      USE_PROM_METRICS: "true"
+      ENABLE_CONVERSIONS: true
+      USE_PROM_METRICS: true
       AWS_S3_USER_FILES_STORAGE_CLASS: REDUCED_REDUNDANCY
       AWS_S3_USER_FILES_BUCKET_NAME: fake-user-files
       AWS_S3_USER_FILES_DEK_BUCKET_NAME: fake-user-files-dek
@@ -73,7 +79,7 @@ services:
         condition: service_completed_successfully
       gcs:
         condition: service_healthy
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   certs:
     build:

+ 7 - 7
services/filestore/package.json

@@ -6,9 +6,9 @@
   "type": "module",
   "scripts": {
     "test:acceptance:run": "mocha --recursive --timeout 15000  $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:acceptance": "yarn run test:acceptance:_run -- --grep=$MOCHA_GREP",
     "test:unit:run": "mocha --recursive $@ test/unit/js",
-    "test:unit": "npm run test:unit:_run",
+    "test:unit": "yarn run test:unit:_run",
     "start": "node app.js",
     "nodemon": "node --watch app.js",
     "lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
@@ -19,11 +19,11 @@
   },
   "dependencies": {
     "@aws-sdk/client-s3": "^3.994.0",
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/object-persistor": "*",
-    "@overleaf/settings": "*",
+    "@overleaf/logger": "workspace:*",
+    "@overleaf/metrics": "workspace:*",
+    "@overleaf/o-error": "workspace:*",
+    "@overleaf/object-persistor": "workspace:*",
+    "@overleaf/settings": "workspace:*",
     "@overleaf/stream-utils": "^0.1.0",
     "body-parser": "1.20.4",
     "bunyan": "^1.8.15",

+ 25 - 0
services/filestore/test/unit/setup.js

@@ -4,7 +4,32 @@ import mongodb from 'mongodb'
 import sinonChai from 'sinon-chai'
 import chaiAsPromised from 'chai-as-promised'
 
+// Workaround: vitest's built-in chai plugins register spy-related properties
+// (e.g. callCount) as getter-only via addChainableMethod. sinon-chai then tries
+// to overwrite them via addMethod (plain property assignment), which fails.
+// Pre-delete the conflicting getter-only properties so the assignment succeeds.
+const sinonChaiMethodProps = [
+  'callCount',
+  'calledBefore',
+  'calledAfter',
+  'calledImmediatelyBefore',
+  'calledImmediatelyAfter',
+  'calledOn',
+  'calledWith',
+  'calledOnceWith',
+  'calledWithExactly',
+  'calledOnceWithExactly',
+  'calledWithMatch',
+  'returned',
+  'thrown',
+]
+for (const name of sinonChaiMethodProps) {
+  if (Object.getOwnPropertyDescriptor(chai.Assertion.prototype, name)?.get) {
+    delete chai.Assertion.prototype[name]
+  }
+}
 chai.use(sinonChai)
+
 chai.use(chaiAsPromised)
 
 // ensure every ObjectId has the id string as a property for correct comparisons

+ 5 - 3
services/history-v1/Dockerfile

@@ -5,6 +5,8 @@
 FROM node:24.14.1 AS base
 
 WORKDIR /overleaf/services/history-v1
+ENV PATH="/overleaf/node_modules/.bin:$PATH"
+RUN corepack enable
 COPY services/history-v1/install_deps.sh /overleaf/services/history-v1/
 RUN chmod 0755 ./install_deps.sh && ./install_deps.sh
 
@@ -17,7 +19,7 @@ RUN mkdir /buckets && chown node:node /buckets
 
 FROM base AS app
 
-COPY package.json package-lock.json /overleaf/
+COPY package.json yarn.lock .yarnrc.yml /overleaf/
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
 COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
@@ -32,10 +34,10 @@ COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/packag
 COPY libraries/validation-tools/package.json /overleaf/libraries/validation-tools/package.json
 COPY services/history-v1/package.json /overleaf/services/history-v1/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
-COPY patches/ /overleaf/patches/
+COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && npm ci --quiet
+RUN cd /overleaf && yarn workspaces focus overleaf-editor overleaf
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
 COPY libraries/metrics/ /overleaf/libraries/metrics/

+ 12 - 12
services/history-v1/Makefile

@@ -15,7 +15,7 @@ IMAGE_REPO ?= us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME)
 IMAGE_REPO_FINAL ?= $(IMAGE_REPO):$(BRANCH_NAME_TAG_SAFE)-$(BUILD_NUMBER)
 IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
-	$(MONOREPO)/package-lock.json \
+	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/fetch-utils/package.json \
 	$(MONOREPO)/libraries/logger/package.json \
 	$(MONOREPO)/libraries/metrics/package.json \
@@ -30,7 +30,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/validation-tools/package.json \
 	$(MONOREPO)/services/history-v1/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
-	$(MONOREPO)/patches/* \
+	$(MONOREPO)/.yarn/patches/* \
 | sha256sum | cut -d '-' -f1)
 
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
@@ -57,11 +57,11 @@ clean:
 
 # Run the linting commands in the scope of the monorepo.
 # Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) --user node node:24.14.1 npm run --silent
-RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) --user node node:24.14.1 npm run --silent
+RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
+RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) -e PATH=$(MONOREPO)/node_modules/.bin:$$PATH --user node node:24.14.1 corepack yarn run --silent
 
-RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) npm run --silent
-RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) npm run --silent
+RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache $(IMAGE_CI) corepack yarn run --silent
+RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/node_modules/.cache:/overleaf/node_modules/.cache -w /overleaf $(IMAGE_CI) corepack yarn run --silent
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -100,19 +100,19 @@ $(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
 	mkdir -p $@
 
 format:
-	$(RUN_LINTING_MONOREPO) format -- services/history-v1
+	$(RUN_LINTING_MONOREPO) format services/history-v1
 
 format_ci:
-	$(RUN_LINTING_CI_MONOREPO) format -- services/history-v1
+	$(RUN_LINTING_CI_MONOREPO) format services/history-v1
 
 format_fix:
-	$(RUN_LINTING_MONOREPO) format:fix -- services/history-v1
+	$(RUN_LINTING_MONOREPO) format:fix services/history-v1
 
 lint:
 	$(RUN_LINTING) lint
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint -- --format json --output-file reports/eslint.json
+	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
@@ -150,7 +150,7 @@ endif
 
 test_acceptance_run_debug:
 ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance corepack yarn run --inspect=0.0.0.0:19999 --inspect-brk test:acceptance
 endif
 
 test_clean: test_acceptance_clean
@@ -163,7 +163,7 @@ ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
 endif
 
 benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
+	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance corepack yarn run benchmarks
 
 build:
 	docker build \

+ 1 - 0
services/history-v1/buildscript.txt

@@ -4,6 +4,7 @@ history-v1
 --env-pass-through=
 --esmock-loader=False
 --node-version=24.14.1
+--package-name=overleaf-editor
 --pipeline-owner=44
 --public-repo=False
 --tsconfig-extra-includes=backup-deletion-app.mjs,backup-verifier-app.mjs,backup-worker-app.mjs,api/**/*,migrations/**/*,storage/**/*

+ 2 - 2
services/history-v1/docker-compose.ci.yml

@@ -13,7 +13,7 @@ services:
       - ./reports:/overleaf/services/history-v1/reports
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:unit:_run
+    command: yarn run test:unit:_run
     environment:
       CI:
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
@@ -77,7 +77,7 @@ services:
         condition: service_healthy
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run test:acceptance
+    command: yarn run test:acceptance
 
   tar:
     build: .

+ 8 - 2
services/history-v1/docker-compose.yml

@@ -15,6 +15,9 @@ services:
       - .:/overleaf/services/history-v1
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/history-v1
@@ -30,7 +33,7 @@ services:
       ANALYTICS_QUEUES_REDIS_HOST: redis_test
       ANALYTICS_QUEUES_BQ_SINK_REDIS_HOST: redis_test
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:unit
+    command: corepack yarn run --silent test:unit
     user: node
     depends_on:
       mongo:
@@ -47,6 +50,9 @@ services:
       - .:/overleaf/services/history-v1
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../package.json:/overleaf/package.json
+      - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
+      - ../../yarn.lock:/overleaf/yarn.lock
       - minio-certs:/certs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
@@ -91,7 +97,7 @@ services:
       gcs:
         condition: service_healthy
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
-    command: npm run --silent test:acceptance
+    command: corepack yarn run --silent test:acceptance
 
   redis_test:
     image: redis:7.4.8

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio