Эх сурвалжийг харах

Acf pnp migration master (#34040)

GitOrigin-RevId: 418eab9bc733f27a6e0f8f04a2a8e21c2f5a491f
Anna Claire Fields 1 сар өмнө
parent
commit
8f07308342
93 өөрчлөгдсөн 1065 нэмэгдсэн , 154 устгасан
  1. 5 0
      .yarn/patches/esmock-npm-2.7.3-83624b28f6.patch
  2. 66 1
      .yarnrc.yml
  3. 0 1
      dockerfiles/cypress/Dockerfile
  4. 6 0
      libraries/access-token-encryptor/package.json
  5. 2 0
      libraries/fetch-utils/package.json
  6. 6 0
      libraries/logger/package.json
  7. 3 0
      libraries/metrics/package.json
  8. 6 0
      libraries/mongo-utils/package.json
  9. 1 0
      libraries/o-error/package.json
  10. 6 0
      libraries/object-persistor/package.json
  11. 8 0
      libraries/overleaf-editor-core/package.json
  12. 2 0
      libraries/promise-utils/package.json
  13. 3 0
      libraries/ranges-tracker/package.json
  14. 2 0
      libraries/redis-wrapper/package.json
  15. 12 2
      libraries/settings/Settings.js
  16. 2 0
      libraries/settings/package.json
  17. 2 0
      libraries/stream-utils/package.json
  18. 3 0
      libraries/validation-tools/package.json
  19. 13 1
      package.json
  20. 6 3
      server-ce/Dockerfile
  21. 1 0
      server-ce/test/.gitignore
  22. 0 1
      server-ce/test/Dockerfile
  23. 4 1
      server-ce/test/cypress.config.ts
  24. 12 8
      server-ce/test/docker-compose.yml
  25. 7 2
      server-ce/test/package.json
  26. 3 2
      services/chat/Dockerfile
  27. 5 3
      services/chat/Makefile
  28. 8 4
      services/chat/docker-compose.yml
  29. 2 0
      services/chat/package.json
  30. 1 1
      services/chat/test/acceptance/js/helpers/MongoHelper.js
  31. 8 7
      services/clsi/Dockerfile
  32. 5 3
      services/clsi/Makefile
  33. 8 4
      services/clsi/docker-compose.yml
  34. 2 0
      services/clsi/package.json
  35. 3 2
      services/docstore/Dockerfile
  36. 5 3
      services/docstore/Makefile
  37. 8 4
      services/docstore/docker-compose.yml
  38. 3 0
      services/docstore/package.json
  39. 1 1
      services/docstore/test/acceptance/js/helpers/MongoHelper.js
  40. 3 2
      services/document-updater/Dockerfile
  41. 5 3
      services/document-updater/Makefile
  42. 8 4
      services/document-updater/docker-compose.yml
  43. 5 0
      services/document-updater/package.json
  44. 4 1
      services/document-updater/scripts/flush_projects_with_no_history_id.js
  45. 1 1
      services/document-updater/test/acceptance/js/helpers/MongoHelper.js
  46. 4 3
      services/filestore/Dockerfile
  47. 5 3
      services/filestore/Makefile
  48. 8 4
      services/filestore/docker-compose.yml
  49. 2 0
      services/filestore/package.json
  50. 3 2
      services/history-v1/Dockerfile
  51. 5 3
      services/history-v1/Makefile
  52. 8 4
      services/history-v1/docker-compose.yml
  53. 6 0
      services/history-v1/package.json
  54. 3 2
      services/history-v1/storage/scripts/back_fill_file_hash.mjs
  55. 3 1
      services/history-v1/storage/scripts/fix_string_backedUpBlobs_ids.mjs
  56. 3 2
      services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  57. 1 1
      services/history-v1/test/acceptance/js/storage/recover_zip.test.mjs
  58. 1 1
      services/history-v1/test/setup.js
  59. 3 2
      services/notifications/Dockerfile
  60. 5 3
      services/notifications/Makefile
  61. 8 4
      services/notifications/docker-compose.yml
  62. 3 0
      services/notifications/package.json
  63. 1 1
      services/notifications/test/acceptance/js/MongoHelper.ts
  64. 8 3
      services/project-history/Dockerfile
  65. 7 3
      services/project-history/Makefile
  66. 1 0
      services/project-history/buildscript.txt
  67. 8 4
      services/project-history/docker-compose.yml
  68. 5 1
      services/project-history/package.json
  69. 1 1
      services/project-history/test/acceptance/js/helpers/MongoHelper.js
  70. 3 2
      services/real-time/Dockerfile
  71. 5 3
      services/real-time/Makefile
  72. 8 4
      services/real-time/docker-compose.yml
  73. 3 0
      services/real-time/package.json
  74. 1 0
      services/web/.gitignore
  75. 3 0
      services/web/.storybook/main.ts
  76. 0 1
      services/web/Dockerfile
  77. 4 3
      services/web/Makefile
  78. 1 1
      services/web/bin/check_extracted_translations
  79. 4 1
      services/web/cypress.config.ts
  80. 22 9
      services/web/docker-compose.yml
  81. 26 3
      services/web/package.json
  82. 1 1
      services/web/scripts/learn/checkSanitize/checkSanitizeOptions.mjs
  83. 113 0
      services/web/scripts/materialize-cypress-reporter.js
  84. 12 0
      services/web/test/acceptance/bootstrap.js
  85. 2 1
      services/web/test/acceptance/src/DeleteOrphanedDocsOnlineCheckTests.mjs
  86. 1 1
      services/web/test/acceptance/src/helpers/MongoHelper.mjs
  87. 2 0
      services/web/test/unit/src/Settings/Settings.test.mjs
  88. 3 0
      services/web/tsconfig.json
  89. 11 0
      services/web/types/uppy-utils.d.ts
  90. 19 6
      services/web/webpack.config.js
  91. 3 0
      tools/migrations/package.json
  92. 1 0
      tsconfig.backend.json
  93. 449 5
      yarn.lock

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 5 - 0
.yarn/patches/esmock-npm-2.7.3-83624b28f6.patch


+ 66 - 1
.yarnrc.yml

@@ -7,10 +7,75 @@ enableScripts: true
 
 enableTelemetry: false
 
-nodeLinker: node-modules
+nodeLinker: pnp
 
 npmMinimalAgeGate: 3d
 
+packageExtensions:
+  "bootstrap@*":
+    dependencies:
+      "@popperjs/core": "*"
+      jquery: "*"
+  "@overleaf/settings@*":
+    peerDependencies:
+      "@overleaf/logger": "*"
+  "mongodb@*":
+    peerDependencies:
+      aws4: "*"
+  "bunyan@*":
+    peerDependencies:
+      source-map-support: "*"
+  "mocha-multi-reporters@*":
+    peerDependencies:
+      mocha-junit-reporter: "*"
+  "mongodb-legacy@*":
+    peerDependencies:
+      mongodb: "*"
+  "knex@*":
+    dependencies:
+      pg-query-stream: "*"
+  "zod-validation-error@*":
+    peerDependencies:
+      zod: "*"
+  "@vitejs/plugin-react@*":
+    peerDependencies:
+      vite: "*"
+  "@babel/preset-env@*":
+    peerDependencies:
+      core-js: "*"
+  "express@*":
+    dependencies:
+      pug: "*"
+  "postcss-loader@*":
+    peerDependencies:
+      autoprefixer: "*"
+      tailwindcss: "*"
+  "@ai-sdk/gateway@*":
+    peerDependencies:
+      zod: "*"
+  "@ai-sdk/provider-utils@*":
+    peerDependencies:
+      zod: "*"
+  "ai@*":
+    peerDependencies:
+      zod: "*"
+  "@uppy/react@*":
+    peerDependencies:
+      "@uppy/status-bar": "*"
+  "inversify@*":
+    dependencies:
+      reflect-metadata: "*"
+  "sinon-mongoose@*":
+    dependencies:
+      mongoose: "*"
+  "react-linkify@*":
+    peerDependencies:
+      react: "*"
+  "debug@*":
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
 supportedArchitectures:
   cpu:
     - current

+ 0 - 1
dockerfiles/cypress/Dockerfile

@@ -3,7 +3,6 @@ ARG USER_UID=1000
 ARG USER_GID=1000
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0

+ 6 - 0
libraries/access-token-encryptor/package.json

@@ -3,6 +3,10 @@
   "version": "3.0.0",
   "description": "",
   "main": "index.js",
+  "exports": {
+    ".": "./index.js",
+    "./*": "./*"
+  },
   "scripts": {
     "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
@@ -17,8 +21,10 @@
     "lodash": "^4.18.1"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 2 - 0
libraries/fetch-utils/package.json

@@ -14,10 +14,12 @@
   "author": "Overleaf (https://www.overleaf.com)",
   "license": "AGPL-3.0-only",
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "@types/node-fetch": "^2.6.13",
     "body-parser": "1.20.4",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "express": "4.22.1",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",

+ 6 - 0
libraries/logger/package.json

@@ -7,6 +7,10 @@
     "url": "https://github.com/overleaf/overleaf"
   },
   "main": "index.js",
+  "exports": {
+    ".": "./index.js",
+    "./*": "./*"
+  },
   "license": "AGPL-3.0-only",
   "version": "3.1.1",
   "scripts": {
@@ -24,7 +28,9 @@
     "bunyan": "^1.8.14"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 3 - 0
libraries/metrics/package.json

@@ -21,8 +21,11 @@
     "yn": "^3.1.1"
   },
   "devDependencies": {
+    "@overleaf/logger": "workspace:*",
+    "@types/node": "^24.5.2",
     "bunyan": "^1.0.0",
     "chai": "^4.3.6",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 6 - 0
libraries/mongo-utils/package.json

@@ -3,6 +3,10 @@
   "version": "0.0.1",
   "description": "utilities to help working with mongo",
   "main": "index.js",
+  "exports": {
+    ".": "./index.js",
+    "./*": "./*"
+  },
   "scripts": {
     "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
@@ -18,7 +22,9 @@
     "mongodb-legacy": "6.1.3"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 1 - 0
libraries/o-error/package.json

@@ -32,6 +32,7 @@
     "@types/chai": "^4.3.0",
     "@types/node": "^24.5.2",
     "chai": "^4.3.6",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

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

@@ -3,6 +3,10 @@
   "version": "1.0.2",
   "description": "Module for storing objects in multiple backends, with fallback on 404 to assist migration between them",
   "main": "index.js",
+  "exports": {
+    ".": "./index.js",
+    "./*": "./*"
+  },
   "scripts": {
     "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "test:unit": "mocha --exit test/**/*.{js,cjs}",
@@ -33,8 +37,10 @@
     "tiny-async-pool": "^1.1.0"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 8 - 0
libraries/overleaf-editor-core/package.json

@@ -3,6 +3,10 @@
   "version": "1.0.0",
   "description": "Library shared between the editor server and clients.",
   "main": "index.js",
+  "exports": {
+    ".": "./index.js",
+    "./*": "./*"
+  },
   "scripts": {
     "test": "yarn run lint && yarn run types:check && yarn run test:unit",
     "lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
@@ -16,8 +20,12 @@
   "private": true,
   "devDependencies": {
     "@types/check-types": "^7.3.7",
+    "@types/lodash": "^4.17.24",
+    "@types/mocha": "^10.0.6",
+    "@types/node": "^24.5.2",
     "@types/path-browserify": "^1.0.3",
     "chai": "^3.3.0",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

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

@@ -14,8 +14,10 @@
   "author": "Overleaf (https://www.overleaf.com)",
   "license": "AGPL-3.0-only",
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.10",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 3 - 0
libraries/ranges-tracker/package.json

@@ -17,7 +17,10 @@
     "types:check": "tsc --noEmit"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
+    "core-js": "~3.46.0",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 2 - 0
libraries/redis-wrapper/package.json

@@ -32,7 +32,9 @@
   "devDependencies": {
     "@overleaf/logger": "workspace:*",
     "@overleaf/o-error": "workspace:*",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 12 - 2
libraries/settings/Settings.js

@@ -1,6 +1,7 @@
 /* eslint-disable no-console */
 const fs = require('node:fs')
 const Path = require('node:path')
+const { createRequire } = require('node:module')
 const { merge } = require('./merge')
 
 const CWD = process.cwd()
@@ -16,6 +17,15 @@ if (SHARELATEX_CONFIG && SHARELATEX_CONFIG !== OVERLEAF_CONFIG) {
   )
 }
 
+// Under Yarn PnP, require() resolves dependencies based on the issuer package.
+// Config files live in the consuming service (e.g. web), not in @overleaf/settings,
+// so we create a require function anchored at each config file's location to ensure
+// dependencies resolve from the service's PnP scope rather than this library's.
+function requireFromFile(filePath) {
+  const absPath = Path.resolve(filePath)
+  return createRequire(absPath)(absPath)
+}
+
 let settings
 let settingsExist = false
 const defaultsPath =
@@ -25,7 +35,7 @@ const defaultsPath =
   pathIfExists(Path.join(ENTRY_POINT_DIR, 'config/settings.defaults.js'))
 if (defaultsPath) {
   console.log(`Using default settings from ${defaultsPath}`)
-  settings = require(defaultsPath)
+  settings = requireFromFile(defaultsPath)
   settingsExist = true
 } else {
   settings = {}
@@ -37,7 +47,7 @@ const overridesPath =
   pathIfExists(Path.join(CWD, `config/settings.${NODE_ENV}.js`))
 if (overridesPath) {
   console.log(`Using settings from ${overridesPath}`)
-  settings = merge(require(overridesPath), settings)
+  settings = merge(requireFromFile(overridesPath), settings)
   settingsExist = true
 }
 

+ 2 - 0
libraries/settings/package.json

@@ -13,6 +13,8 @@
     "types:check": "tsc --noEmit"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

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

@@ -14,8 +14,10 @@
   "author": "Overleaf (https://www.overleaf.com)",
   "license": "AGPL-3.0-only",
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

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

@@ -24,6 +24,9 @@
     "zod-validation-error": "^4.0.1"
   },
   "devDependencies": {
+    "@types/express": "4.17.23",
+    "@types/node": "^24.5.2",
+    "eslint": "^10.4.0",
     "typescript": "^5.0.4",
     "vitest": "4.1.5"
   }

+ 13 - 1
package.json

@@ -26,6 +26,7 @@
     "eslint-plugin-promise": "^7.2.1",
     "eslint-plugin-unicorn": "^56.0.0",
     "globals": "^17.6.0",
+    "js-yaml": "^4.1.1",
     "prettier": "3.7.4",
     "prettier-plugin-groovy": "0.2.1",
     "typescript": "^5.9.3"
@@ -131,6 +132,7 @@
     "cache-flow": "1.9.0",
     "focus-trap-react": "11.0.4",
     "jose": "4.15.5",
+    "js-yaml": "^4.1.1",
     "nodemailer": "8.0.5",
     "on-headers": "1.0.2",
     "pug": "3.0.3",
@@ -196,6 +198,7 @@
   },
   "workspaces": [
     "jobs/mirror-documentation",
+    "jobs/wiki-algolia",
     "libraries/*",
     "server-ce/test",
     "services/analytics",
@@ -212,6 +215,7 @@
     "services/history-v1",
     "services/idp",
     "services/latexqc",
+    "services/linked-url-proxy",
     "services/notifications",
     "services/project-history",
     "services/real-time",
@@ -223,5 +227,13 @@
     "tools/npm-overrides-helper",
     "tools/saas-e2e",
     "tools/migrations"
-  ]
+  ],
+  "dependenciesMeta": {
+    "cypress-multi-reporters@2.0.5": {
+      "unplugged": true
+    },
+    "esmock@2.7.3": {
+      "unplugged": true
+    }
+  }
 }

+ 6 - 3
server-ce/Dockerfile

@@ -10,13 +10,12 @@ WORKDIR /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 --parents libraries/*/package.json .yarn/patches/ services/*/package.json tools/migrations/ package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY server-ce/genScript.js server-ce/services.js /overleaf/
 
 # Pre-install yarn via corepack so it is available at runtime for all users
 # -------------------------------------------------------------------------
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -34,10 +33,14 @@ COPY --parents libraries/ services/ tools/migrations/ /overleaf/
 RUN --mount=type=cache,target=/root/.cache \
     --mount=type=cache,target=/root/.yarn/berry/cache,id=server-ce-yarn-cache \
     --mount=type=cache,target=/usr/local/share/.cache/yarn,id=server-ce-yarn-fallback-cache \
-    --mount=type=cache,target=/overleaf/services/web/node_modules/.cache,id=server-ce-webpack-cache \
+    --mount=type=cache,target=/overleaf/services/web/.cache,id=server-ce-webpack-cache \
     --mount=type=tmpfs,target=/tmp \
     node genScript compile | bash
 
+# Enable Yarn PnP resolution for all Node.js processes
+# -----------------------------------------------------
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 # Copy runit service startup scripts to its location
 # --------------------------------------------------
 ADD server-ce/runit /etc/service

+ 1 - 0
server-ce/test/.gitignore

@@ -1,2 +1,3 @@
 data/
 docker-mailtrap/
+cypress/reporter/

+ 0 - 1
server-ce/test/Dockerfile

@@ -1,7 +1,6 @@
 FROM node:24.14.1
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0

+ 4 - 1
server-ce/test/cypress.config.ts

@@ -33,10 +33,13 @@ if (process.env.CYPRESS_SHARD && !process.env.SPEC_PATTERN) {
 
 const specPattern = process.env.SPEC_PATTERN || './**/*.spec.ts'
 
+// Cypress's Electron process uses PackherdModuleLoader which doesn't go through
+// PnP hooks. Reporter packages are materialized into /tmp/cypress-reporter by
+// scripts/materialize-cypress-reporter.cjs at container startup.
 let reporterOptions = {}
 if (process.env.CI) {
   reporterOptions = {
-    reporter: `${process.env.MONOREPO}/node_modules/cypress-multi-reporters`,
+    reporter: '/tmp/cypress-reporter/node_modules/cypress-multi-reporters',
     reporterOptions: {
       configFile: 'cypress/cypress-multi-reporters.cjs',
     },

+ 12 - 8
server-ce/test/docker-compose.yml

@@ -79,12 +79,15 @@ services:
     working_dir: $PWD
     volumes:
       - $PWD:$PWD
-      - $MONOREPO/libraries:$MONOREPO/libraries
-      - $MONOREPO/node_modules:$MONOREPO/node_modules
-      - $MONOREPO/.yarn:$MONOREPO/.yarn
-      - $MONOREPO/.yarnrc.yml:$MONOREPO/.yarnrc.yml
-      - $MONOREPO/package.json:$MONOREPO/package.json
-      - $MONOREPO/yarn.lock:$MONOREPO/yarn.lock
+      - $MONOREPO/libraries:$MONOREPO/libraries:ro
+      - $MONOREPO/scripts:$MONOREPO/scripts:ro
+      - $MONOREPO/.yarn:$MONOREPO/.yarn:ro
+      - $MONOREPO/.yarnrc.yml:$MONOREPO/.yarnrc.yml:ro
+      - $MONOREPO/.pnp.cjs:$MONOREPO/.pnp.cjs:ro
+      - $MONOREPO/.pnp.loader.mjs:$MONOREPO/.pnp.loader.mjs:ro
+      - $MONOREPO/.pnp.register.mjs:$MONOREPO/.pnp.register.mjs:ro
+      - $MONOREPO/package.json:$MONOREPO/package.json:ro
+      - $MONOREPO/yarn.lock:$MONOREPO/yarn.lock:ro
     environment:
       MONOREPO:
       CYPRESS_SHARD:
@@ -108,15 +111,16 @@ services:
 
   host-admin:
     build: .
-    entrypoint: ["node", "--watch", "host-admin.js"]
+    entrypoint: ["yarn", "node", "--watch", "host-admin.js"]
     # See comment in Makefile regarding matching file paths
     working_dir: $PWD
     volumes:
       - $PWD:$PWD
       - $MONOREPO/libraries:$MONOREPO/libraries:ro
-      - $MONOREPO/node_modules:$MONOREPO/node_modules:ro
       - $MONOREPO/.yarn:$MONOREPO/.yarn:ro
       - $MONOREPO/.yarnrc.yml:$MONOREPO/.yarnrc.yml:ro
+      - $MONOREPO/.pnp.cjs:$MONOREPO/.pnp.cjs:ro
+      - $MONOREPO/.pnp.loader.mjs:$MONOREPO/.pnp.loader.mjs:ro
       - $MONOREPO/package.json:$MONOREPO/package.json:ro
       - $MONOREPO/yarn.lock:$MONOREPO/yarn.lock:ro
       - /var/run/docker.sock:/var/run/docker.sock

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

@@ -4,8 +4,9 @@
   "private": true,
   "type": "module",
   "scripts": {
-    "cypress:open": "cypress open --e2e --browser chrome",
-    "cypress:run": "cypress run --e2e --browser chrome",
+    "materialize-reporter": "node ../../scripts/materialize-cypress-reporter.cjs /tmp/cypress-reporter",
+    "cypress:open": "yarn run materialize-reporter && cypress open --e2e --browser chrome",
+    "cypress:run": "yarn run materialize-reporter && cypress run --e2e --browser chrome",
     "lint": "eslint --cache --cache-location ../../.cache/eslint/ --max-warnings 0 --format unix --ext .js,.jsx,.mjs,.ts,.tsx .",
     "lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .js,.jsx,.mjs,.ts,.tsx ."
   },
@@ -20,11 +21,15 @@
     "body-parser": "1.20.4",
     "cypress": "15.12.0",
     "cypress-multi-reporters": "^2.0.5",
+    "eslint": "^10.4.0",
+    "eslint-formatter-unix": "^8.40.0",
     "express": "4.22.1",
     "isomorphic-git": "^1.33.1",
     "js-yaml": "^4.1.1",
+    "mocha": "^11.7.5",
     "mocha-junit-reporter": "^2.2.1",
     "pdfjs-dist": "^5.1.91",
+    "typescript": "^5.0.4",
     "uuid": "^9.0.1",
     "zod-validation-error": "^4.0.1"
   }

+ 3 - 2
services/chat/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -44,6 +43,8 @@ COPY libraries/settings/ /overleaf/libraries/settings/
 COPY services/chat/ /overleaf/services/chat/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/chat/Makefile

@@ -69,9 +69,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/chat/reports:/overleaf/services/chat/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -119,14 +121,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/chat
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/chat
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/chat/reports/eslint.json services/chat
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/chat
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/chat
@@ -24,7 +26,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: yarn run --silent test:unit
     user: node
@@ -39,12 +41,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/chat
@@ -55,7 +59,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 2 - 0
services/chat/package.json

@@ -29,10 +29,12 @@
   },
   "devDependencies": {
     "@overleaf/migrations": "workspace:*",
+    "@types/node": "^24.5.2",
     "acorn": "^7.1.1",
     "ajv": "^6.12.0",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 1 - 1
services/chat/test/acceptance/js/helpers/MongoHelper.js

@@ -5,6 +5,6 @@ before('run migrations', async function () {
   this.timeout(60_000)
 
   await promisify(exec)(
-    `cd ../../tools/migrations && npm run migrations -- migrate -t server-ce`
+    `cd ../../tools/migrations && yarn run migrations migrate -t server-ce`
   )
 })

+ 8 - 7
services/clsi/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -22,7 +21,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -49,20 +48,22 @@ COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/
 COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/clsi/ /overleaf/services/clsi/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app AS with-texlive
 
 RUN apt-get update \
-    && apt-cache depends texlive-full | grep "Depends: " | grep -v -- "-doc" | grep -v -- "-lang-" | sed 's/Depends: //' | xargs apt-get install -y --no-install-recommends \
-    && apt-get install -y --no-install-recommends fontconfig inkscape python3-pygments qpdf \
-    && rm -rf /var/lib/apt/lists/*
+  && apt-cache depends texlive-full | grep "Depends: " | grep -v -- "-doc" | grep -v -- "-lang-" | sed 's/Depends: //' | xargs apt-get install -y --no-install-recommends \
+  && apt-get install -y --no-install-recommends fontconfig inkscape python3-pygments qpdf \
+  && rm -rf /var/lib/apt/lists/*
 
 RUN mkdir -p cache compiles output \
-&&  chown node:node cache compiles output
+  &&  chown node:node cache compiles output
 
 CMD ["node", "--expose-gc", "app.js"]
 
 FROM app
 RUN mkdir -p cache compiles output \
-&&  chown node:node cache compiles output
+  &&  chown node:node cache compiles output
 
 CMD ["node", "--expose-gc", "app.js"]

+ 5 - 3
services/clsi/Makefile

@@ -71,9 +71,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/clsi/reports:/overleaf/services/clsi/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -121,14 +123,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/clsi
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/clsi
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/clsi/reports/eslint.json services/clsi
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/clsi
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     working_dir: /overleaf/services/clsi
     environment:
@@ -23,7 +25,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     command: yarn run --silent test:unit
 
   test_acceptance:
@@ -33,12 +35,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - /var/run/docker.sock:/var/run/docker.sock
     working_dir: /overleaf/services/clsi
     environment:
@@ -48,7 +52,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       DOWNLOAD_HOST: http://clsi-nginx:8080
       ALLOWED_COMPILE_GROUPS: clsi-perf simple-latex-file
       ENABLE_PDF_CACHING: true

+ 2 - 0
services/clsi/package.json

@@ -41,8 +41,10 @@
   },
   "devDependencies": {
     "@istanbuljs/esm-loader-hook": "^0.3.0",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "form-data": "^4.0.5",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",

+ 3 - 2
services/docstore/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -50,6 +49,8 @@ COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/docstore/ /overleaf/services/docstore/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/docstore/Makefile

@@ -72,9 +72,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/docstore/reports:/overleaf/services/docstore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -122,14 +124,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/docstore
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/docstore
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/docstore/reports/eslint.json services/docstore
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/docstore
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
@@ -25,7 +27,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: yarn run --silent test:unit
     user: node
@@ -40,12 +42,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/docstore
@@ -59,7 +63,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 3 - 0
services/docstore/package.json

@@ -31,14 +31,17 @@
     "bunyan": "^1.8.15",
     "express": "4.22.1",
     "lodash": "^4.18.1",
+    "mongodb": "6.12.0",
     "mongodb-legacy": "6.1.3",
     "p-map": "^4.0.0"
   },
   "devDependencies": {
     "@google-cloud/storage": "^7.19.0",
     "@overleaf/migrations": "workspace:*",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 1 - 1
services/docstore/test/acceptance/js/helpers/MongoHelper.js

@@ -5,6 +5,6 @@ before('run migrations', async function () {
   this.timeout(60_000)
 
   await promisify(exec)(
-    `cd ../../tools/migrations && npm run migrations -- migrate -t server-ce`
+    `cd ../../tools/migrations && yarn run migrations migrate -t server-ce`
   )
 })

+ 3 - 2
services/document-updater/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -50,6 +49,8 @@ COPY libraries/settings/ /overleaf/libraries/settings/
 COPY services/document-updater/ /overleaf/services/document-updater/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/document-updater/Makefile

@@ -72,9 +72,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/reports:/overleaf/services/document-updater/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -122,14 +124,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/document-updater
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/document-updater
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/document-updater/reports/eslint.json services/document-updater
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/document-updater
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/document-updater
@@ -24,7 +26,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       HISTORY_REDIS_HOST: redis_test
       QUEUES_REDIS_HOST: redis_test
@@ -46,12 +48,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/document-updater
@@ -67,7 +71,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 5 - 0
services/document-updater/package.json

@@ -34,15 +34,20 @@
     "express": "4.22.1",
     "lodash": "^4.18.1",
     "minimist": "^1.2.8",
+    "mongodb": "6.12.0",
     "mongodb-legacy": "6.1.3",
     "overleaf-editor-core": "workspace:*"
   },
   "devDependencies": {
     "@overleaf/migrations": "workspace:*",
+    "@types/ioredis": "^4.28.10",
+    "@types/mocha": "^10.0.6",
+    "@types/node": "^24.5.2",
     "basic-auth": "^2.0.1",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "cluster-key-slot": "^1.0.5",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 4 - 1
services/document-updater/scripts/flush_projects_with_no_history_id.js

@@ -204,7 +204,10 @@ async function scanNodes(nodes, batchSize = 1000) {
       }
     }
 
-    logger.info({ scanned, server: node.serverInfo.role }, 'Scanned node')
+    logger.info(
+      { scanned, server: /** @type {any} */ (node).serverInfo.role },
+      'Scanned node'
+    )
   }
 }
 

+ 1 - 1
services/document-updater/test/acceptance/js/helpers/MongoHelper.js

@@ -5,6 +5,6 @@ before('run migrations', async function () {
   this.timeout(60_000)
 
   await promisify(exec)(
-    `cd ../../tools/migrations && npm run migrations -- migrate -t server-ce`
+    `cd ../../tools/migrations && yarn run migrations migrate -t server-ce`
   )
 })

+ 4 - 3
services/filestore/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -20,7 +19,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -43,9 +42,11 @@ COPY libraries/settings/ /overleaf/libraries/settings/
 COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/
 COPY services/filestore/ /overleaf/services/filestore/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 RUN mkdir -p uploads template_files \
-&&  chown node:node uploads template_files
+  &&  chown node:node uploads template_files
 USER node
 
 CMD ["node", "--expose-gc", "app.js"]

+ 5 - 3
services/filestore/Makefile

@@ -69,9 +69,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/filestore/reports:/overleaf/services/filestore/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -119,14 +121,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/filestore
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/filestore
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/filestore/reports/eslint.json services/filestore
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/filestore
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -13,12 +13,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     working_dir: /overleaf/services/filestore
     environment:
@@ -26,7 +28,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     command: yarn run --silent test:unit
     user: node
 
@@ -37,12 +39,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - minio-certs:/certs
     working_dir: /overleaf/services/filestore
     environment:
@@ -62,7 +66,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       ENABLE_CONVERSIONS: true
       USE_PROM_METRICS: true
       AWS_S3_USER_FILES_STORAGE_CLASS: REDUCED_REDUNDANCY

+ 2 - 0
services/filestore/package.json

@@ -36,8 +36,10 @@
   },
   "devDependencies": {
     "@google-cloud/storage": "^7.19.0",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 3 - 2
services/history-v1/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -23,7 +22,7 @@ RUN mkdir /buckets && chown node:node /buckets
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -59,6 +58,8 @@ COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/history-v1/ /overleaf/services/history-v1/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/history-v1/Makefile

@@ -74,9 +74,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/history-v1/reports:/overleaf/services/history-v1/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -124,14 +126,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/history-v1
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/history-v1
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/history-v1/reports/eslint.json services/history-v1
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/history-v1
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -13,12 +13,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/history-v1
@@ -27,7 +29,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       HISTORY_REDIS_HOST: redis_test
       QUEUES_REDIS_HOST: redis_test
@@ -49,12 +51,14 @@ services:
       target: base
     volumes:
       - .:/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
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - minio-certs:/certs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
@@ -81,7 +85,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 6 - 0
services/history-v1/package.json

@@ -7,6 +7,7 @@
   "private": true,
   "dependencies": {
     "@google-cloud/secret-manager": "^5.6.0",
+    "@google-cloud/storage": "^7.19.0",
     "@overleaf/fetch-utils": "workspace:*",
     "@overleaf/logger": "workspace:*",
     "@overleaf/metrics": "workspace:*",
@@ -48,11 +49,16 @@
     "zip-stream": "^7.0.2"
   },
   "devDependencies": {
+    "@aws-sdk/client-s3": "^3.994.0",
     "@overleaf/migrations": "workspace:*",
+    "@types/chai-as-promised": "^7.1.8",
+    "@types/mocha": "^10.0.6",
+    "@types/node": "^24.5.2",
     "benny": "^3.7.1",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "chai-exclude": "^2.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 3 - 2
services/history-v1/storage/scripts/back_fill_file_hash.mjs

@@ -316,8 +316,9 @@ if (OUTPUT_FILE !== '-') {
 logger.initialize('file-migration', {
   streams: [
     {
-      stream:
-        OUTPUT_FILE === '-'
+      stream: DISPLAY_REPORT
+        ? process.stderr
+        : OUTPUT_FILE === '-'
           ? process.stdout
           : fs.createWriteStream(OUTPUT_FILE, { flags: 'a' }),
     },

+ 3 - 1
services/history-v1/storage/scripts/fix_string_backedUpBlobs_ids.mjs

@@ -1,9 +1,11 @@
 // @ts-check
 import { backedUpBlobs } from '../lib/mongodb.js'
-import { mongoId } from '../lib/assert.js'
+import assert from '../lib/assert.js'
 import { ObjectId } from 'mongodb'
 import commandLineArgs from 'command-line-args'
 
+const { mongoId } = assert
+
 const STATS = {
   total: 0,
   replaced: 0,

+ 3 - 2
services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs

@@ -485,7 +485,7 @@ describe('back_fill_file_hash script', function () {
    * @param {Array<string>} args
    * @return {Promise<{result: { stdout: string, stderr: string, status: number }, stats: any}>}
    */
-  async function rawRunScript(args = []) {
+  async function rawRunScript(args = [], env = {}) {
     let result
     try {
       result = await promisify(execFile)(
@@ -502,6 +502,7 @@ describe('back_fill_file_hash script', function () {
             ...process.env,
             AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE: '1',
             LOG_LEVEL: 'warn', // Override LOG_LEVEL of acceptance tests
+            ...env,
           },
         }
       )
@@ -1120,7 +1121,7 @@ describe('back_fill_file_hash script', function () {
     let output
     before('prepare environment', prepareEnvironment)
     before('run script', async function () {
-      output = await rawRunScript(['--report'])
+      output = await rawRunScript(['--report'], { LOG_LEVEL: 'error' })
     })
     it('should print the report', () => {
       expect(output.status).to.equal(0)

+ 1 - 1
services/history-v1/test/acceptance/js/storage/recover_zip.test.mjs

@@ -140,7 +140,7 @@ async function runRecoverZipScript(args) {
   try {
     result = await promisify(execFile)(
       'node',
-      ['storage/scripts/recover_zip.js', ...args],
+      ['--no-deprecation', 'storage/scripts/recover_zip.js', ...args],
       {
         encoding: 'utf-8',
         timeout: TIMEOUT,

+ 1 - 1
services/history-v1/test/setup.js

@@ -22,7 +22,7 @@ async function setupMongoDatabase() {
   this.timeout(60_000)
   await promisify(exec)(
     // Run saas migrations for backup indexes
-    `cd ../../tools/migrations && npm run migrations -- migrate -t saas`
+    `cd ../../tools/migrations && yarn run migrations migrate -t saas`
   )
 }
 

+ 3 - 2
services/notifications/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -46,6 +45,8 @@ COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/notifications/ /overleaf/services/notifications/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/notifications/Makefile

@@ -70,9 +70,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/notifications/reports:/overleaf/services/notifications/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/notifications/reports:/overleaf/services/notifications/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/notifications/reports:/overleaf/services/notifications/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -120,14 +122,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/notifications
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/notifications
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/notifications/reports/eslint.json services/notifications
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/notifications
 
 typecheck:
 	$(RUN_LINTING) types:check

+ 8 - 4
services/notifications/docker-compose.yml

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/notifications
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
@@ -25,7 +27,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: yarn run --silent test:unit
     user: node
@@ -40,12 +42,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/notifications
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
@@ -57,7 +61,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 3 - 0
services/notifications/package.json

@@ -30,14 +30,17 @@
     "body-parser": "1.20.4",
     "bunyan": "^1.8.15",
     "express": "4.22.1",
+    "mongodb": "6.12.0",
     "mongodb-legacy": "6.1.3",
     "zod": "^4.1.7",
     "zod-validation-error": "^4.0.1"
   },
   "devDependencies": {
     "@overleaf/migrations": "workspace:*",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "typescript": "^5.0.4",
     "vitest": "4.1.5"

+ 1 - 1
services/notifications/test/acceptance/js/MongoHelper.ts

@@ -4,6 +4,6 @@ import { promisify } from 'node:util'
 
 beforeAll(async function () {
   await promisify(exec)(
-    `cd ../../tools/migrations && npm run migrations -- migrate -t server-ce`
+    `cd ../../tools/migrations && yarn run migrations migrate -t server-ce`
   )
 }, 60_000)

+ 8 - 3
services/project-history/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -27,16 +26,18 @@ COPY libraries/mongo-utils/package.json /overleaf/libraries/mongo-utils/package.
 COPY libraries/o-error/package.json /overleaf/libraries/o-error/package.json
 COPY libraries/overleaf-editor-core/package.json /overleaf/libraries/overleaf-editor-core/package.json
 COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/package.json
+COPY libraries/ranges-tracker/package.json /overleaf/libraries/ranges-tracker/package.json
 COPY libraries/redis-wrapper/package.json /overleaf/libraries/redis-wrapper/package.json
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
 COPY libraries/validation-tools/package.json /overleaf/libraries/validation-tools/package.json
+COPY services/document-updater/package.json /overleaf/services/document-updater/package.json
 COPY services/project-history/package.json /overleaf/services/project-history/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
 COPY .yarn/patches/ /overleaf/.yarn/patches/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
-RUN cd /overleaf && yarn workspaces focus @overleaf/project-history overleaf
+RUN cd /overleaf && yarn workspaces focus @overleaf/project-history @overleaf/document-updater overleaf
 COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
 COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
 COPY libraries/logger/ /overleaf/libraries/logger/
@@ -45,13 +46,17 @@ COPY libraries/mongo-utils/ /overleaf/libraries/mongo-utils/
 COPY libraries/o-error/ /overleaf/libraries/o-error/
 COPY libraries/overleaf-editor-core/ /overleaf/libraries/overleaf-editor-core/
 COPY libraries/promise-utils/ /overleaf/libraries/promise-utils/
+COPY libraries/ranges-tracker/ /overleaf/libraries/ranges-tracker/
 COPY libraries/redis-wrapper/ /overleaf/libraries/redis-wrapper/
 COPY libraries/settings/ /overleaf/libraries/settings/
 COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/
 COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
+COPY services/document-updater/ /overleaf/services/document-updater/
 COPY services/project-history/ /overleaf/services/project-history/
 COPY tools/migrations/ /overleaf/tools/migrations/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 7 - 3
services/project-history/Makefile

@@ -24,10 +24,12 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/o-error/package.json \
 	$(MONOREPO)/libraries/overleaf-editor-core/package.json \
 	$(MONOREPO)/libraries/promise-utils/package.json \
+	$(MONOREPO)/libraries/ranges-tracker/package.json \
 	$(MONOREPO)/libraries/redis-wrapper/package.json \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/libraries/stream-utils/package.json \
 	$(MONOREPO)/libraries/validation-tools/package.json \
+	$(MONOREPO)/services/document-updater/package.json \
 	$(MONOREPO)/services/project-history/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
 	$(MONOREPO)/.yarn/patches/* \
@@ -73,9 +75,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/app/js/types.ts:/overleaf/services/document-updater/app/js/types.ts --volume $(MONOREPO)/services/project-history/reports:/overleaf/services/project-history/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/app/js/types.ts:/overleaf/services/document-updater/app/js/types.ts --volume $(MONOREPO)/services/project-history/reports:/overleaf/services/project-history/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/document-updater/app/js/types.ts:/overleaf/services/document-updater/app/js/types.ts --volume $(MONOREPO)/services/project-history/reports:/overleaf/services/project-history/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -123,14 +127,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/project-history
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/project-history
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/project-history/reports/eslint.json services/project-history
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/project-history
 
 typecheck:
 	$(RUN_LINTING) types:check

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

@@ -4,6 +4,7 @@ project-history
 --env-add=
 --env-pass-through=
 --esmock-loader=True
+--extra-focus-packages=@overleaf/document-updater
 --node-version=24.14.1
 --package-name=@overleaf/project-history
 --pipeline-owner=44

+ 8 - 4
services/project-history/docker-compose.yml

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/project-history
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/project-history
@@ -24,7 +26,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       HISTORY_REDIS_HOST: redis_test
       QUEUES_REDIS_HOST: redis_test
@@ -46,12 +48,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/project-history
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
     working_dir: /overleaf/services/project-history
@@ -67,7 +71,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       mongo:

+ 5 - 1
services/project-history/package.json

@@ -33,18 +33,22 @@
     "express": "4.22.1",
     "lodash": "^4.18.1",
     "minimist": "^1.2.8",
+    "mongodb": "6.12.0",
     "mongodb-legacy": "6.1.3",
     "overleaf-editor-core": "workspace:*",
     "p-queue": "^8.1.0"
   },
   "devDependencies": {
     "@overleaf/migrations": "workspace:*",
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
-    "esmock": "^2.7.3",
+    "eslint": "^10.4.0",
+    "esmock": "patch:esmock@npm%3A2.7.3#~/.yarn/patches/esmock-npm-2.7.3-83624b28f6.patch",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",
+    "mongodb": "6.12.0",
     "nock": "^13.5.3",
     "sinon": "~9.0.1",
     "sinon-chai": "^3.7.0",

+ 1 - 1
services/project-history/test/acceptance/js/helpers/MongoHelper.js

@@ -5,6 +5,6 @@ before('run migrations', async function () {
   this.timeout(60_000)
 
   await promisify(exec)(
-    `cd ../../tools/migrations && npm run migrations -- migrate -t server-ce`
+    `cd ../../tools/migrations && yarn run migrations migrate -t server-ce`
   )
 })

+ 3 - 2
services/real-time/Dockerfile

@@ -5,7 +5,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0
@@ -18,7 +17,7 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
 
 FROM base AS app
 
-COPY package.json yarn.lock .yarnrc.yml /overleaf/
+COPY package.json yarn.lock .yarnrc.yml .pnp.register.mjs /overleaf/
 COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
 COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
 COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
@@ -43,6 +42,8 @@ COPY libraries/settings/ /overleaf/libraries/settings/
 COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/real-time/ /overleaf/services/real-time/
 
+ENV NODE_OPTIONS="--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+
 FROM app
 USER node
 

+ 5 - 3
services/real-time/Makefile

@@ -69,9 +69,11 @@ clean:
 # Eslint and prettier (plus some configs) are on the root.
 RUN_LINTING = ../../bin/run -w /overleaf/services/$(PROJECT_NAME) monorepo yarn run --silent
 RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
+RUN_EXEC_MONOREPO = ../../bin/run monorepo yarn exec
 
 RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/real-time/reports:/overleaf/services/real-time/reports --volume $(MONOREPO)/.cache:/overleaf/.cache $(IMAGE_CI) yarn run --silent
 RUN_LINTING_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/real-time/reports:/overleaf/services/real-time/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn run --silent
+RUN_EXEC_CI_MONOREPO = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/eslint.config.mjs:/overleaf/eslint.config.mjs --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json --volume $(MONOREPO)/services/real-time/reports:/overleaf/services/real-time/reports --volume $(MONOREPO)/.cache:/overleaf/.cache -w /overleaf $(IMAGE_CI) yarn exec
 
 SHELLCHECK_OPTS = \
 	--shell=bash \
@@ -119,14 +121,14 @@ format_fix:
 	$(RUN_LINTING_MONOREPO) format:fix services/real-time
 
 lint:
-	$(RUN_LINTING) lint
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format unix services/real-time
 
 lint_ci:
-	-$(RUN_LINTING_CI) lint --format json --output-file reports/eslint.json
+	-$(RUN_EXEC_CI_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --max-warnings 0 --format json --output-file services/real-time/reports/eslint.json services/real-time
 	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
 
 lint_fix:
-	$(RUN_LINTING) lint:fix
+	$(RUN_EXEC_MONOREPO) eslint --cache --cache-location ./.cache/eslint/ --fix services/real-time
 
 typecheck:
 	$(RUN_LINTING) types:check

+ 8 - 4
services/real-time/docker-compose.yml

@@ -10,12 +10,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/real-time
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
     working_dir: /overleaf/services/real-time
     environment:
@@ -23,7 +25,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       HISTORY_REDIS_HOST: redis_test
       QUEUES_REDIS_HOST: redis_test
@@ -42,12 +44,14 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/real-time
-      - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
       - ../../yarn.lock:/overleaf/yarn.lock
       - ../../.yarn:/overleaf/.yarn
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
     working_dir: /overleaf/services/real-time
     environment:
       RETRIES:
@@ -61,7 +65,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
     user: node
     depends_on:
       redis_test:

+ 3 - 0
services/real-time/package.json

@@ -36,12 +36,15 @@
     "proxy-addr": "^2.0.7",
     "socket.io": "github:overleaf/socket.io#0.9.19-overleaf-12",
     "socket.io-client": "github:overleaf/socket.io-client#0.9.17-overleaf-5",
+    "zod": "^3.24.0",
     "zod-validation-error": "^4.0.1"
   },
   "devDependencies": {
+    "@types/node": "^24.5.2",
     "chai": "^4.3.6",
     "chai-as-promised": "^7.1.1",
     "cookie-signature": "^1.1.0",
+    "eslint": "^10.4.0",
     "mocha": "^11.1.0",
     "mocha-junit-reporter": "^2.2.1",
     "mocha-multi-reporters": "^1.5.1",

+ 1 - 0
services/web/.gitignore

@@ -22,6 +22,7 @@ cypress/screenshots/
 cypress/videos/
 cypress/downloads/
 cypress/results/
+cypress/reporter/
 
 # Ace themes for conversion
 frontend/js/features/source-editor/themes/ace/

+ 3 - 0
services/web/.storybook/main.ts

@@ -127,6 +127,9 @@ export default defineMain({
             rootDir,
             'modules/writefull/frontend/js/integration/src/'
           ),
+          // Allow all packages to resolve core-js (injected by Babel's
+          // useBuiltIns: 'usage') even under Yarn PnP strict resolution.
+          'core-js': path.dirname(require.resolve('core-js/package.json')),
         },
       },
       module: {

+ 0 - 1
services/web/Dockerfile

@@ -4,7 +4,6 @@
 FROM node:24.14.1 AS base
 
 # Corepack setup, shared between all the images.
-ENV PATH="/overleaf/node_modules/.bin:$PATH"
 ENV COREPACK_HOME=/opt/corepack
 RUN corepack enable && corepack install -g yarn@4.14.1
 ENV COREPACK_ENABLE_NETWORK=0

+ 4 - 3
services/web/Makefile

@@ -499,9 +499,9 @@ RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
 
 ORG_PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 RUN_LINT_FORMAT ?= \
-	docker run --rm --env BRANCH_NAME --env CI --env COMMIT_SHA --env MONOREPO --env PATH=/overleaf/node_modules/.bin:$(ORG_PATH) --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume ${PWD}/data/reports:/overleaf/services/web/data/reports --volume $(MONOREPO)/.cache/:/overleaf/.cache/ ${IMAGE_CI}
+	docker run --rm --env BRANCH_NAME --env CI --env COMMIT_SHA --env MONOREPO --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc.cjs:/overleaf/.prettierrc.cjs --volume ${PWD}/data/reports:/overleaf/services/web/data/reports --volume $(MONOREPO)/.cache/:/overleaf/.cache/ ${IMAGE_CI}
 
-NODE_MODULES_PATH := ${PATH}:${PWD}/node_modules/.bin:/overleaf/services/web/node_modules/.bin
+NODE_MODULES_PATH := ${PATH}
 WITH_NODE_MODULES_PATH = \
 	format_backend \
 	format_frontend \
@@ -583,7 +583,7 @@ lint_flag_res_send_usage:
 
 lint: lint_overleafModuleImports
 lint_overleafModuleImports:
-	node scripts/check_overleafModuleImports.mjs
+	yarn node scripts/check_overleafModuleImports.mjs
 
 lint: typecheck_frontend
 typecheck_frontend:
@@ -668,6 +668,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/package.json \
 	$(MONOREPO)/yarn.lock \
 	$(MONOREPO)/libraries/access-token-encryptor/package.json \
+	$(MONOREPO)/libraries/eslint-config/package.json \
   $(MONOREPO)/libraries/eslint-plugin/package.json \
   $(MONOREPO)/libraries/fetch-utils/package.json \
   $(MONOREPO)/libraries/logger/package.json \

+ 1 - 1
services/web/bin/check_extracted_translations

@@ -5,7 +5,7 @@ set -e
 # Ensure all locales used in the frontend are tracked
 OUTPUT=data/dumpFolder/i18next-scanner
 trap 'rm -rf "$OUTPUT"' EXIT
-npx i18next-scanner --output "$OUTPUT"
+yarn exec i18next-scanner --output "$OUTPUT"
 ACTUAL=frontend/extracted-translations.json
 EXPECTED="$OUTPUT/frontend/extracted-translations.json"
 if ! diff "$ACTUAL" "$EXPECTED"; then

+ 4 - 1
services/web/cypress.config.ts

@@ -1,10 +1,13 @@
 import { defineConfig } from 'cypress'
 import { webpackConfig } from './cypress/support/webpack.cypress'
 
+// Cypress's Electron process uses PackherdModuleLoader which doesn't go through
+// PnP hooks. Reporter packages are materialized into /tmp/cypress-reporter by
+// scripts/materialize-cypress-reporter.js at container startup.
 let reporterOptions = {}
 if (process.env.CI) {
   reporterOptions = {
-    reporter: 'cypress-multi-reporters',
+    reporter: '/tmp/cypress-reporter/node_modules/cypress-multi-reporters',
     reporterOptions: {
       configFile: 'cypress/cypress-multi-reporters.json',
     },

+ 22 - 9
services/web/docker-compose.yml

@@ -9,7 +9,9 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/web
-      - ../../node_modules:/overleaf/node_modules
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../libraries:/overleaf/libraries
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
@@ -30,7 +32,7 @@ services:
       MOCHA_GREP: ${MOCHA_GREP:-}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       MONOREPO:
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
@@ -47,7 +49,9 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/web
-      - ../../node_modules:/overleaf/node_modules
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../libraries:/overleaf/libraries
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
       - ../../tools/migrations:/overleaf/tools/migrations
@@ -69,6 +73,7 @@ services:
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_SERVER_SELECTION_TIMEOUT: 600000
       MONGO_SOCKET_TIMEOUT: 300000
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       REDIS_HOST: redis_test
       MONOREPO:
       RETRIES:
@@ -91,7 +96,9 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/web
-      - ../../node_modules:/overleaf/node_modules
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
@@ -102,7 +109,7 @@ services:
       CI:
       JUNIT_ROOT_SUITE_NAME:
       MOCHA_GREP: ${MOCHA_GREP}
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       VERBOSE_LOGGING:
     command: yarn run --silent test:frontend
     user: node
@@ -115,7 +122,10 @@ services:
         - USER_GID
     volumes:
       - .:/overleaf/services/web
-      - ../../node_modules:/overleaf/node_modules
+      - ../../scripts:/overleaf/scripts:ro
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
@@ -128,7 +138,8 @@ services:
       JUNIT_ROOT_SUITE_NAME:
       CYPRESS_SPEC_PATTERN: ${CYPRESS_SPEC_PATTERN:-}
       CYPRESS_EXCLUDE_SPEC_PATTERN: ${CYPRESS_EXCLUDE_SPEC_PATTERN:-}
-    entrypoint: npm
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs"
+    entrypoint: yarn
     command:
       - "run"
       - "cypress:run-ct"
@@ -140,7 +151,9 @@ services:
       target: base
     volumes:
       - .:/overleaf/services/web
-      - ../../node_modules:/overleaf/node_modules
+      - ../../.pnp.cjs:/overleaf/.pnp.cjs
+      - ../../.pnp.loader.mjs:/overleaf/.pnp.loader.mjs
+      - ../../.pnp.register.mjs:/overleaf/.pnp.register.mjs
       - ../../libraries:/overleaf/libraries
       - ../../package.json:/overleaf/package.json
       - ../../.yarnrc.yml:/overleaf/.yarnrc.yml
@@ -150,7 +163,7 @@ services:
     environment:
       CI:
       JUNIT_ROOT_SUITE_NAME:
-      NODE_OPTIONS: "--unhandled-rejections=strict"
+      NODE_OPTIONS: "--require /overleaf/.pnp.cjs --import /overleaf/.pnp.register.mjs --unhandled-rejections=strict"
       VERBOSE_LOGGING:
     command: yarn run --silent test:writefull
     user: node

+ 26 - 3
services/web/package.json

@@ -3,6 +3,10 @@
   "description": "The HTTP front end for Overleaf",
   "private": true,
   "main": "app.mjs",
+  "exports": {
+    ".": "./app.mjs",
+    "./*": "./*"
+  },
   "directories": {
     "public": "./public"
   },
@@ -36,8 +40,9 @@
     "type-check:backend": "tsc -p tsconfig.backend.json --noEmit",
     "extract-translations": "i18next-scanner",
     "convert-themes": "node frontend/js/features/source-editor/themes/scripts/convert.js",
-    "cypress:open-ct": "OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js cypress open --component",
-    "cypress:run-ct": "OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js cypress run --component --browser chrome",
+    "materialize-reporter": "node scripts/materialize-cypress-reporter.js /tmp/cypress-reporter",
+    "cypress:open-ct": "yarn run materialize-reporter && OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js cypress open --component",
+    "cypress:run-ct": "yarn run materialize-reporter && OVERLEAF_CONFIG=$(pwd)/config/settings.webpack.js cypress run --component --browser chrome",
     "cypress:docker:open-ct": "USER_UID=$(id -u) USER_GID=$(id -g) docker compose -f docker-compose.cypress.yml run --rm cypress yarn run cypress:open-ct",
     "cypress:docker:run-ct": "USER_UID=$(id -u) USER_GID=$(id -g) docker compose -f docker-compose.cypress.yml run --rm cypress yarn run cypress:run-ct --browser chrome",
     "lezer-latex:generate": "node scripts/lezer-latex/generate.mjs",
@@ -152,6 +157,7 @@
     "minimist": "^1.2.7",
     "mmmagic": "^0.5.3",
     "moment": "^2.29.4",
+    "mongodb": "6.12.0",
     "mongodb-legacy": "6.1.3",
     "mongoose": "8.22.1",
     "multer": "2.1.1",
@@ -198,6 +204,7 @@
     "@ai-sdk/react": "^3.0.172",
     "@babel/core": "^7.28.5",
     "@babel/plugin-proposal-decorators": "^7.28.0",
+    "@babel/plugin-transform-typescript": "^7.28.0",
     "@babel/preset-env": "^7.28.5",
     "@babel/preset-react": "^7.28.5",
     "@babel/preset-typescript": "^7.28.5",
@@ -235,6 +242,7 @@
     "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#371ce3b56f453a392eb0d3b85ab019c185c68e1f",
     "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",
     "@sentry/browser": "7.46.0",
+    "@sentry/types": "7.46.0",
     "@storybook/addon-a11y": "10.3.5",
     "@storybook/addon-designs": "^11.1.3",
     "@storybook/addon-docs": "10.3.5",
@@ -256,15 +264,19 @@
     "@types/async": "^3.2.25",
     "@types/bootstrap": "^5.2.10",
     "@types/chai": "^4.3.0",
+    "@types/chai-as-promised": "^8.0.2",
     "@types/dateformat": "^5.0.3",
     "@types/diff": "^5.2.3",
     "@types/dom-speech-recognition": "^0.0.7",
     "@types/events": "^3.0.3",
     "@types/express": "^4.17.23",
+    "@types/lodash": "^4.17.24",
+    "@types/mdast": "^4.0.4",
     "@types/minimist": "^1.2.5",
     "@types/mocha": "^9.1.0",
     "@types/mocha-each": "^2.0.4",
     "@types/node": "^24.5.2",
+    "@types/path-browserify": "^1.0.3",
     "@types/react": "^18.3.20",
     "@types/react-color": "^3.0.13",
     "@types/react-dom": "^18.3.6",
@@ -273,15 +285,19 @@
     "@types/react-syntax-highlighter": "^15.5.11",
     "@types/recurly__recurly-js": "^4.38.0",
     "@types/sanitize-html": "^2.14.0",
+    "@types/sinon": "^21.0.1",
     "@types/sinon-chai": "^3.2.12",
     "@types/utf-8-validate": "^5.0.2",
     "@types/uuid": "^10.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.59.4",
+    "@typescript-eslint/parser": "^8.59.4",
     "@uppy/core": "^3.8.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",
+    "@uppy/status-bar": "^3.2.5",
     "@uppy/utils": "^5.7.0",
     "@uppy/xhr-upload": "3.6.0",
     "@vitest/coverage-istanbul": "^4.0.17",
@@ -316,6 +332,8 @@
     "core-js": "~3.46.0",
     "css-loader": "^7.1.4",
     "css-minimizer-webpack-plugin": "^8.0.0",
+    "csv-parse": "^6.2.1",
+    "csv-stringify": "^6.7.0",
     "cypress": "15.12.0",
     "cypress-multi-reporters": "^2.0.5",
     "cypress-plugin-tab": "^1.0.5",
@@ -326,9 +344,12 @@
     "downshift": "^9.0.9",
     "es6-promise": "^4.2.8",
     "escodegen": "^2.0.0",
+    "eslint": "^10.4.0",
     "eslint-config-prettier": "^10.1.8",
+    "eslint-formatter-unix": "^8.40.0",
     "eslint-plugin-chai-expect": "^4.1.0",
     "eslint-plugin-chai-friendly": "^1.2.0",
+    "eslint-plugin-cypress": "^4.1.0",
     "eslint-plugin-import": "^2.32.0",
     "eslint-plugin-jsx-a11y": "^6.10.2",
     "eslint-plugin-mocha": "^11.3.0",
@@ -344,6 +365,7 @@
     "fake-indexeddb": "^6.0.0",
     "fast-fuzzy": "^1.12.0",
     "fetch-mock": "^12.6.0",
+    "focus-trap": "^8.2.1",
     "formik": "^2.2.9",
     "glob": "^12.0.0",
     "globals": "^17.6.0",
@@ -419,8 +441,9 @@
     "timekeeper": "^2.2.0",
     "to-string-loader": "^1.2.0",
     "ts-loader": "^9.5.7",
+    "tslib": "^2.8.1",
     "tty-browserify": "^0.0.1",
-    "typescript": "^5.9.3",
+    "typescript": "^5.0.4",
     "unified": "^11.0.5",
     "unist-util-visit": "^5.0.0",
     "use-stick-to-bottom": "^1.1.1",

+ 1 - 1
services/web/scripts/learn/checkSanitize/checkSanitizeOptions.mjs

@@ -3,7 +3,7 @@ import fs from 'node:fs'
 import Path from 'node:path'
 import cheerio from 'cheerio'
 // checkSanitizeOptions is only used in dev env
-// eslint-disable-next-line import/no-extraneous-dependencies
+// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
 import * as prettier from 'prettier'
 import sanitizeHtml from 'sanitize-html'
 import { sanitizeOptions } from '../../../modules/learn/app/src/sanitizeOptions.mjs'

+ 113 - 0
services/web/scripts/materialize-cypress-reporter.js

@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+'use strict'
+/**
+ * Materialize Cypress reporter packages (and their transitive dependencies)
+ * into a real node_modules tree so that Cypress's Electron process (which uses
+ * PackherdModuleLoader, NOT Yarn PnP hooks) can load them.
+ *
+ * Builds a NESTED node_modules structure (each package gets its own
+ * node_modules/ for its deps) to avoid version conflicts from flat dedup.
+ *
+ * The target-dir must be OUTSIDE the PnP workspace root — otherwise PnP
+ * intercepts requires from the materialized packages and blocks undeclared deps.
+ *
+ * Usage:  yarn node scripts/materialize-cypress-reporter.js <target-dir>
+ */
+
+const path = require('path')
+const fs = require('fs')
+
+const targetDir = process.argv[2]
+if (!targetDir) {
+  console.error(
+    'Usage: yarn node scripts/materialize-cypress-reporter.js <target-dir>'
+  )
+  process.exit(1)
+}
+
+const pnp = require('module').findPnpApi(__filename)
+if (!pnp) {
+  console.log('Not running under PnP — nothing to do.')
+  process.exit(0)
+}
+
+let totalCopied = 0
+
+// PnP patches readFileSync/readdirSync/statSync but NOT cpSync.
+// Manual recursive copy to read from virtual/zip paths.
+function copyRecursive(src, dest) {
+  const stat = fs.statSync(src)
+  if (stat.isDirectory()) {
+    fs.mkdirSync(dest, { recursive: true })
+    for (const entry of fs.readdirSync(src)) {
+      copyRecursive(path.join(src, entry), path.join(dest, entry))
+    }
+  } else {
+    fs.mkdirSync(path.dirname(dest), { recursive: true })
+    fs.writeFileSync(dest, fs.readFileSync(src))
+  }
+}
+
+function resolvePkgDir(pkgName, issuer) {
+  // Use resolveToUnqualified to get the package directory without going
+  // through the package's exports map (which may not expose package.json).
+  let pkgDir = pnp.resolveToUnqualified(pkgName, issuer + '/')
+  if (pkgDir.endsWith('/')) pkgDir = pkgDir.slice(0, -1)
+  return pkgDir
+}
+
+/**
+ * Materialize a package and its deps into destDir/node_modules/pkgName.
+ * Each package's own deps get nested under its own node_modules/.
+ */
+function materializeRecursive(pkgName, issuer, destNodeModules, seen) {
+  // Dedupe by destination path to avoid infinite loops, but allow the same
+  // package to appear in multiple locations (nested node_modules).
+  const key = `${destNodeModules}/${pkgName}`
+  if (seen.has(key)) return
+  seen.add(key)
+
+  let pkgDir
+  try {
+    pkgDir = resolvePkgDir(pkgName, issuer)
+  } catch {
+    return // optional/missing dep
+  }
+
+  const dest = path.join(destNodeModules, pkgName)
+  copyRecursive(pkgDir, dest)
+  totalCopied++
+
+  const pkg = JSON.parse(
+    fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')
+  )
+  const deps = [
+    ...Object.keys(pkg.dependencies || {}),
+    ...Object.keys(pkg.peerDependencies || {}),
+  ]
+
+  if (deps.length > 0) {
+    const nestedNodeModules = path.join(dest, 'node_modules')
+    for (const dep of deps) {
+      materializeRecursive(dep, pkgDir, nestedNodeModules, seen)
+    }
+  }
+}
+
+const rootNodeModules = path.resolve(targetDir, 'node_modules')
+const seen = new Set()
+const topLevel = [
+  'cypress-multi-reporters',
+  'mocha-junit-reporter',
+  'typescript',
+]
+
+for (const pkg of topLevel) {
+  try {
+    materializeRecursive(pkg, process.cwd(), rootNodeModules, seen)
+  } catch (e) {
+    console.warn(`Warning: could not materialize ${pkg}: ${e.message}`)
+  }
+}
+
+console.log(`Materialized ${totalCopied} packages into ${rootNodeModules}`)

+ 12 - 0
services/web/test/acceptance/bootstrap.js

@@ -1,3 +1,15 @@
+// Register PnP ESM loader via module.register() for synchronous ESM resolution
+// (--experimental-loader from yarn only works for async resolution)
+const { register } = require('node:module')
+const { pathToFileURL } = require('node:url')
+const path = require('node:path')
+const pnpRegister = path.resolve(__dirname, '../../.pnp.register.mjs')
+try {
+  register(pathToFileURL(pnpRegister), pathToFileURL(__filename))
+} catch {
+  // .pnp.register.mjs may not exist in non-PnP environments
+}
+
 const chai = require('chai')
 chai.should()
 chai.use(require('chai-as-promised'))

+ 2 - 1
services/web/test/acceptance/src/DeleteOrphanedDocsOnlineCheckTests.mjs

@@ -119,7 +119,8 @@ describe('DeleteOrphanedDocsOnlineCheck', function () {
             // Hide verbose log messages `calling destroy for project in docstore`
             'LOG_LEVEL=error',
             // Hide deprecation warnings for calling `db.collection.count`
-            'NODE_OPTIONS=--no-deprecation',
+            // Preserve existing NODE_OPTIONS (e.g. PnP hooks) while adding --no-deprecation
+            `NODE_OPTIONS='${process.env.NODE_OPTIONS || ''} --no-deprecation'`,
           ])
           .concat(['node', 'scripts/delete_orphaned_docs_online_check.mjs'])
           .join(' ')

+ 1 - 1
services/web/test/acceptance/src/helpers/MongoHelper.mjs

@@ -22,7 +22,7 @@ export default {
       this.timeout(60_000)
 
       await promisify(exec)(
-        `cd ../../tools/migrations && npm run migrations -- migrate -t ${Settings.env || DEFAULT_ENV}`
+        `cd ../../tools/migrations && yarn run migrations migrate -t ${Settings.env || DEFAULT_ENV}`
       )
     })
 

+ 2 - 0
services/web/test/unit/src/Settings/Settings.test.mjs

@@ -10,7 +10,9 @@ function clearSettingsCache() {
   const settingsDeps = Object.keys(require.cache).filter(
     x =>
       x.includes('/@overleaf/settings') ||
+      x.includes('/@overleaf-settings-virtual') ||
       x.includes(`${monorepoPath}/libraries/settings`) ||
+      x.includes('/libraries/settings/') ||
       x.includes(`${monorepoPath}/services/web/config`)
   )
   settingsDeps.forEach(dep => delete require.cache[dep])

+ 3 - 0
services/web/tsconfig.json

@@ -20,6 +20,9 @@
       "@modules/*": ["./modules/*"],
       "@overleaf/o-error": ["../../libraries/o-error"],
       "@overleaf/ranges-tracker": ["../../libraries/ranges-tracker"],
+      "@overleaf/validation-tools": ["../../libraries/validation-tools"],
+      "overleaf-editor-core": ["../../libraries/overleaf-editor-core"],
+      "overleaf-editor-core/*": ["../../libraries/overleaf-editor-core/*"],
 
       "@ol-types/*": ["./types/*"],
       "@ol-storybook/*": ["./.storybook/*"],

+ 11 - 0
services/web/types/uppy-utils.d.ts

@@ -0,0 +1,11 @@
+// Ambient module declaration for @uppy/utils subpath imports.
+// Under Yarn PnP, TypeScript cannot find the ambient declarations in
+// @uppy/utils/types/index.d.ts for deep imports like
+// @uppy/utils/lib/getDroppedFiles. Re-declare the subset we use here.
+declare module '@uppy/utils/lib/getDroppedFiles' {
+  function getDroppedFiles(
+    dataTransfer: DataTransfer,
+    options?: Record<string, unknown>
+  ): Promise<File[]>
+  export default getDroppedFiles
+}

+ 19 - 6
services/web/webpack.config.js

@@ -50,13 +50,11 @@ globSync(
 })
 
 function getModuleDirectory(moduleName) {
-  const entrypointPath = require.resolve(moduleName)
-  const suffix = `node_modules/${moduleName}`
-  const idx = entrypointPath.indexOf(suffix)
-  if (idx === -1) {
-    throw new Error(`could not find Node module: ${moduleName}`)
+  try {
+    return path.dirname(require.resolve(`${moduleName}/package.json`))
+  } catch (err) {
+    throw new Error(`could not find Node module: ${moduleName}`, { cause: err })
   }
-  return entrypointPath.slice(0, idx + suffix.length)
 }
 
 const mathjaxDir = getModuleDirectory('mathjax')
@@ -312,6 +310,21 @@ module.exports = {
       // Ensure all packages use the same jQuery instance (prevents duplicate
       // copies from Yarn hoisting breaking jQuery plugins like daterangepicker)
       jquery: require.resolve('jquery'),
+      // Under Yarn PnP, babel-injected core-js polyfill imports cannot be
+      // resolved from third-party packages (e.g. @uppy, pdfjs-dist) because
+      // they don't declare core-js as a dependency. Alias to the web
+      // workspace's copy so all packages can resolve it.
+      'core-js': path.dirname(require.resolve('core-js/package.json')),
+      // writefull's tsconfig uses importHelpers: true, which emits tslib
+      // imports that must be resolvable from the web workspace.
+      tslib: path.dirname(require.resolve('tslib/package.json')),
+      // Under PnP, packages like cypress/react that import react/react-dom
+      // can't resolve them from their own scope. Alias to web's copies.
+      react: path.dirname(require.resolve('react/package.json')),
+      'react-dom': path.dirname(require.resolve('react-dom/package.json')),
+      // ai-sdk packages declare zod as a peer dep but PnP can't resolve it
+      // from their scope in the Docker build.
+      zod: path.dirname(require.resolve('zod/package.json')),
     },
     // symlinks: false, // enable this while using `npm link`
     extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'],

+ 3 - 0
tools/migrations/package.json

@@ -5,6 +5,9 @@
     "lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
     "lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts ."
   },
+  "devDependencies": {
+    "eslint": "^10.4.0"
+  },
   "dependencies": {
     "@overleaf/logger": "workspace:*",
     "@overleaf/mongo-utils": "workspace:*",

+ 1 - 0
tsconfig.backend.json

@@ -15,6 +15,7 @@
       "@overleaf/logger": ["./libraries/logger"],
       "@overleaf/metrics": ["./libraries/metrics"],
       "@overleaf/mongo-utils": ["./libraries/mongo-utils"],
+      "@overleaf/mongo-utils/*": ["./libraries/mongo-utils/*"],
       "@overleaf/object-persistor": ["./libraries/object-persistor"],
       "@overleaf/o-error": ["./libraries/o-error"],
       "overleaf-editor-core": ["./libraries/overleaf-editor-core"],

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 449 - 5
yarn.lock


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно