From 9a5f5f48f7c8ff9b18c5cabd998d4297b8911857 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 14 May 2026 16:16:36 +0100 Subject: [PATCH 1/6] Support Node 25 with corepack-managed pnpm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/backend-tests.yml | 4 +- .github/workflows/build-and-deploy-docs.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/frontend-admin-tests.yml | 2 +- .github/workflows/frontend-tests.yml | 8 ++-- .github/workflows/handleRelease.yml | 2 +- .github/workflows/installer-test.yml | 4 +- .github/workflows/load-test.yml | 6 +-- .github/workflows/perform-type-check.yml | 2 +- .github/workflows/rate-limit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/releaseEtherpad.yml | 2 +- .github/workflows/update-plugins.yml | 2 +- .../workflows/upgrade-from-latest-release.yml | 2 +- CHANGELOG.md | 2 +- package.json | 43 ++++++++++--------- src/static/js/pluginfw/installer.ts | 3 +- src/static/js/pluginfw/plugins.ts | 7 +-- 18 files changed, 50 insertions(+), 47 deletions(-) diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 96fed832116..4a03f1bc4e3 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: # PRs: test on latest Node only. Push to develop: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[22, 24, 25]') }} + node: ${{ github.event_name == 'pull_request' && fromJSON('[25]') || fromJSON('[22, 24, 25]') }} steps: - name: Checkout repository @@ -84,7 +84,7 @@ jobs: strategy: fail-fast: false matrix: - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[22, 24, 25]') }} + node: ${{ github.event_name == 'pull_request' && fromJSON('[25]') || fromJSON('[22, 24, 25]') }} steps: - name: Checkout repository diff --git a/.github/workflows/build-and-deploy-docs.yml b/.github/workflows/build-and-deploy-docs.yml index 826b4687773..18f23cc6cf5 100644 --- a/.github/workflows/build-and-deploy-docs.yml +++ b/.github/workflows/build-and-deploy-docs.yml @@ -61,7 +61,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Setup Pages if: github.event_name == 'push' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a0c7b33f782..e1ff5630071 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -59,7 +59,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm cache-dependency-path: etherpad/pnpm-lock.yaml - diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 544f801b373..db13a4f918c 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: # PRs: single Node version. Push: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[22, 24, 25]') }} + node: ${{ github.event_name == 'pull_request' && fromJSON('[25]') || fromJSON('[22, 24, 25]') }} steps: - diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 0a349e09105..e667e2c62d2 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -42,7 +42,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite @@ -114,7 +114,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite run: pnpm install --frozen-lockfile @@ -190,7 +190,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite run: pnpm install --frozen-lockfile @@ -291,7 +291,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite run: pnpm install --frozen-lockfile diff --git a/.github/workflows/handleRelease.yml b/.github/workflows/handleRelease.yml index a2a2bac2e67..320e1104fe3 100644 --- a/.github/workflows/handleRelease.yml +++ b/.github/workflows/handleRelease.yml @@ -42,7 +42,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite run: pnpm install --frozen-lockfile diff --git a/.github/workflows/installer-test.yml b/.github/workflows/installer-test.yml index d77c277a18e..843b0fb15f8 100644 --- a/.github/workflows/installer-test.yml +++ b/.github/workflows/installer-test.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 - name: Pre-install pnpm (avoid sudo prompt in the installer) run: npm install -g pnpm @@ -104,7 +104,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 - name: Pre-install pnpm run: npm install -g pnpm diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index a3be2745179..a5431171d5e 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -39,7 +39,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite @@ -77,7 +77,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install etherpad-load-test @@ -140,7 +140,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite diff --git a/.github/workflows/perform-type-check.yml b/.github/workflows/perform-type-check.yml index af155c3273d..c9932f936e6 100644 --- a/.github/workflows/perform-type-check.yml +++ b/.github/workflows/perform-type-check.yml @@ -39,7 +39,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite run: pnpm install --frozen-lockfile diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 2363f98d64b..7e7ad9b744e 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -42,7 +42,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 583067226fa..2dbc95969c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 cache: pnpm cache-dependency-path: etherpad/pnpm-lock.yaml - name: Install dependencies ether.github.com diff --git a/.github/workflows/releaseEtherpad.yml b/.github/workflows/releaseEtherpad.yml index 60d7ce7f5ca..0673f2f97ba 100644 --- a/.github/workflows/releaseEtherpad.yml +++ b/.github/workflows/releaseEtherpad.yml @@ -19,7 +19,7 @@ jobs: # OIDC trusted publishing needs npm >= 11.5.1, which requires # Node >= 22.9.0. setup-node's `22` resolves to the latest # 22.x, which satisfies that. - node-version: 22 + node-version: 25 registry-url: https://registry.npmjs.org/ - name: Upgrade npm to >=11.5.1 (required for trusted publishing) run: npm install -g npm@latest diff --git a/.github/workflows/update-plugins.yml b/.github/workflows/update-plugins.yml index ca9b01844e6..868e381e42a 100644 --- a/.github/workflows/update-plugins.yml +++ b/.github/workflows/update-plugins.yml @@ -26,7 +26,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 25 - name: Install bin dependencies working-directory: ./bin diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 00d2291ec6a..b84d32f7e7d 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: # PRs: single Node version. Push: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[22, 24, 25]') }} + node: ${{ github.event_name == 'pull_request' && fromJSON('[25]') || fromJSON('[22, 24, 25]') }} steps: - name: Check out latest release diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f173fd0e06..fb7225d3897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ - The HTTP client in the backend has been migrated from `axios` to the built-in `fetch` API, dropping a dependency now that Node 22 ships a stable fetch. - `admin/` and `ui/` workspaces moved from `rolldown-vite` to upstream **Vite 8**. -- Build and CI moved to **pnpm 11** (`packageManager: "pnpm@11.0.6"`); the `Dockerfile`, snap, and all GitHub workflows are aligned. pnpm overrides have been migrated from `package.json` to `pnpm-workspace.yaml` to match pnpm 11's expectations. +- Build and CI moved to **pnpm 11** (`packageManager: "pnpm@11.1.2"`); the `Dockerfile`, snap, and all GitHub workflows are aligned. pnpm overrides have been migrated from `package.json` to `pnpm-workspace.yaml` to match pnpm 11's expectations. - All client modules have been converted to ESM. - The CI matrix tests Node 22, 24, and 25; on PRs the matrix is reduced to a single Node version to keep feedback fast. - Frontend Playwright tests now run against the `/ether` plugin set, with feature-tag based skips so plugin-incompatible specs are excluded automatically. diff --git a/package.json b/package.json index 1d18ac543eb..d8db9009e49 100644 --- a/package.json +++ b/package.json @@ -13,25 +13,25 @@ "etherpad-healthcheck": "bin/etherpad-healthcheck" }, "scripts": { - "lint": "pnpm --filter ep_etherpad-lite run lint", - "test": "pnpm --filter ep_etherpad-lite run test", - "test-utils": "pnpm --filter ep_etherpad-lite run test-utils", - "test-container": "pnpm --filter ep_etherpad-lite run test-container", - "dev": "pnpm --filter ep_etherpad-lite run dev", - "prod": "pnpm --filter ep_etherpad-lite run prod", - "ts-check": "pnpm --filter ep_etherpad-lite run ts-check", - "ts-check:watch": "pnpm --filter ep_etherpad-lite run ts-check:watch", - "test-ui": "pnpm --filter ep_etherpad-lite run test-ui", - "test-ui:ui": "pnpm --filter ep_etherpad-lite run test-ui:ui", - "test-admin": "pnpm --filter ep_etherpad-lite run test-admin", - "test-admin:ui": "pnpm --filter ep_etherpad-lite run test-admin:ui", - "plugins": "pnpm --filter bin run plugins", - "install-plugins": "pnpm --filter bin run plugins i", - "remove-plugins": "pnpm --filter bin run remove-plugins", - "list-plugins": "pnpm --filter bin run list-plugins", - "build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy", - "build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy", - "makeDocs": "pnpm --filter bin run makeDocs" + "lint": "corepack pnpm --filter ep_etherpad-lite run lint", + "test": "corepack pnpm --filter ep_etherpad-lite run test", + "test-utils": "corepack pnpm --filter ep_etherpad-lite run test-utils", + "test-container": "corepack pnpm --filter ep_etherpad-lite run test-container", + "dev": "corepack pnpm --filter ep_etherpad-lite run dev", + "prod": "corepack pnpm --filter ep_etherpad-lite run prod", + "ts-check": "corepack pnpm --filter ep_etherpad-lite run ts-check", + "ts-check:watch": "corepack pnpm --filter ep_etherpad-lite run ts-check:watch", + "test-ui": "corepack pnpm --filter ep_etherpad-lite run test-ui", + "test-ui:ui": "corepack pnpm --filter ep_etherpad-lite run test-ui:ui", + "test-admin": "corepack pnpm --filter ep_etherpad-lite run test-admin", + "test-admin:ui": "corepack pnpm --filter ep_etherpad-lite run test-admin:ui", + "plugins": "corepack pnpm --filter bin run plugins", + "install-plugins": "corepack pnpm --filter bin run plugins i", + "remove-plugins": "corepack pnpm --filter bin run remove-plugins", + "list-plugins": "corepack pnpm --filter bin run list-plugins", + "build:etherpad": "corepack pnpm --filter admin run build-copy && corepack pnpm --filter ui run build-copy", + "build:ui": "corepack pnpm --filter ui run build-copy && corepack pnpm --filter admin run build-copy", + "makeDocs": "corepack pnpm --filter bin run makeDocs" }, "dependencies": { "ep_etherpad-lite": "link:src" @@ -42,9 +42,10 @@ "ui": "link:ui" }, "engines": { - "node": ">=22.13.0" + "node": ">=22.13.0", + "pnpm": ">=11.0.0" }, - "packageManager": "pnpm@11.0.6", + "packageManager": "pnpm@11.1.2", "repository": { "type": "git", "url": "https://github.com/ether/etherpad.git" diff --git a/src/static/js/pluginfw/installer.ts b/src/static/js/pluginfw/installer.ts index 8f8f937e701..b68e22dc895 100644 --- a/src/static/js/pluginfw/installer.ts +++ b/src/static/js/pluginfw/installer.ts @@ -20,6 +20,7 @@ import {LinkInstaller} from "./LinkInstaller"; import {findEtherpadRoot} from '../../../node/utils/AbsolutePaths'; const logger = log4js.getLogger('plugins'); +const pnpmCmd = ['corepack', 'pnpm']; export const pluginInstallPath = path.join(settings.root, 'src','plugin_packages'); export const node_modules = path.join(findEtherpadRoot(),'src', 'node_modules'); @@ -59,7 +60,7 @@ const migratePluginsFromNodeModules = async () => { // * The `--no-production` flag is required (or the `NODE_ENV` environment variable must be // unset or set to `development`) because otherwise `npm ls` will not mention any packages // that are not included in `package.json` (which is expected to not exist). - const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production']; + const cmd = [...pnpmCmd, 'ls', '--long', '--json', '--depth=0', '--no-production']; const [{dependencies = {}}] = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']})); diff --git a/src/static/js/pluginfw/plugins.ts b/src/static/js/pluginfw/plugins.ts index f2960138ac8..97b8f2be1a1 100644 --- a/src/static/js/pluginfw/plugins.ts +++ b/src/static/js/pluginfw/plugins.ts @@ -14,6 +14,7 @@ import settings, { } from '../../../node/utils/Settings'; const logger = log4js.getLogger('plugins'); +const pnpmCmd = ['corepack', 'pnpm']; // Log the version of pnpm at startup. pnpm is only used for dev workflows // and plugin migration; it isn't required at runtime (admin-UI plugin @@ -22,11 +23,11 @@ const logger = log4js.getLogger('plugins'); // ERROR in the logs. (async () => { try { - const version = await runCmd(['pnpm', '--version'], {stdio: [null, 'string']}); - logger.info(`pnpm --version: ${version}`); + const version = await runCmd([...pnpmCmd, '--version'], {stdio: [null, 'string']}); + logger.info(`corepack pnpm --version: ${version}`); } catch (err) { if ((err as any).code === 'ENOENT') { - logger.debug('pnpm not found on PATH (only needed for dev workflows)'); + logger.debug('corepack not found on PATH (only needed for dev workflows)'); } else { logger.warn(`Failed to get pnpm version: ${err.stack || err}`); } From 6a4466dc8fe882c3c2ff5f2c7b626bf2a16d0943 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 14 May 2026 16:31:47 +0100 Subject: [PATCH 2/6] Fix Node 25 CI regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/installer-test.yml | 8 ++++---- admin/src/components/SearchField.tsx | 15 +++++++++++++++ admin/src/utils/sorting.ts | 6 ++++++ 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 admin/src/components/SearchField.tsx create mode 100644 admin/src/utils/sorting.ts diff --git a/.github/workflows/installer-test.yml b/.github/workflows/installer-test.yml index 843b0fb15f8..aa4af2192cb 100644 --- a/.github/workflows/installer-test.yml +++ b/.github/workflows/installer-test.yml @@ -50,8 +50,8 @@ jobs: - name: Run bin/installer.sh against this commit env: ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test - ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git - ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }} + ETHERPAD_REPO: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} + ETHERPAD_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} NO_COLOR: "1" run: sh ./bin/installer.sh @@ -113,8 +113,8 @@ jobs: shell: pwsh env: ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test - ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git - ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }} + ETHERPAD_REPO: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} + ETHERPAD_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} NO_COLOR: "1" run: ./bin/installer.ps1 diff --git a/admin/src/components/SearchField.tsx b/admin/src/components/SearchField.tsx new file mode 100644 index 00000000000..e6fb9cde9de --- /dev/null +++ b/admin/src/components/SearchField.tsx @@ -0,0 +1,15 @@ +import type {ChangeEventHandler, FC} from 'react'; +import {Search} from 'lucide-react'; + +export type SearchFieldProps = { + value: string; + onChange: ChangeEventHandler; + placeholder?: string; +}; + +export const SearchField: FC = ({onChange, value, placeholder}) => ( + + + + +); diff --git a/admin/src/utils/sorting.ts b/admin/src/utils/sorting.ts new file mode 100644 index 00000000000..d439ab5e82e --- /dev/null +++ b/admin/src/utils/sorting.ts @@ -0,0 +1,6 @@ +export const determineSorting = (sortBy: string, ascending: boolean, currentSymbol: string) => { + if (sortBy === currentSymbol) { + return ascending ? 'sort up' : 'sort down'; + } + return 'sort none'; +}; From cb84bed31aede60f594d0b4e3379583006e06cf9 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 14 May 2026 16:39:37 +0100 Subject: [PATCH 3/6] Use corepack fallback in operational pnpm paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 ++ bin/functions.sh | 23 +++++++++++++++++++++++ bin/installDeps.sh | 6 +++--- bin/run.sh | 6 +++--- src/node/hooks/express/updateActions.ts | 9 ++++----- src/node/updater/RollbackHandler.ts | 10 ++++++++-- src/node/updater/UpdateExecutor.ts | 15 ++++++++++++--- src/node/updater/index.ts | 9 ++++----- src/node/updater/pnpm.ts | 23 +++++++++++++++++++++++ 9 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 src/node/updater/pnpm.ts diff --git a/README.md b/README.md index 6533e22ab39..7ee7cca0b3a 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,8 @@ git switch -c v2.2.5 ```sh ./bin/run.sh ``` + This uses the pinned pnpm from `package.json`, via `pnpm` when available or + `corepack pnpm` otherwise. 6. Stop with [CTRL-C] 7. Restart your Etherpad service diff --git a/bin/functions.sh b/bin/functions.sh index 3d8bbcadeba..22d0976a8ac 100644 --- a/bin/functions.sh +++ b/bin/functions.sh @@ -12,6 +12,29 @@ error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } is_cmd() { command -v "$@" >/dev/null 2>&1; } +ensure_pnpm() { + if is_cmd pnpm; then return 0; fi + is_cmd corepack || fatal "Please install pnpm or corepack." + corepack pnpm --version >/dev/null 2>&1 || \ + fatal "corepack is available but could not provision pnpm." +} + +run_pnpm() { + if is_cmd pnpm; then + pnpm "$@" + else + corepack pnpm "$@" + fi +} + +exec_pnpm() { + if is_cmd pnpm; then + exec pnpm "$@" + else + exec corepack pnpm "$@" + fi +} + get_program_version() { PROGRAM="$1" diff --git a/bin/installDeps.sh b/bin/installDeps.sh index af2b5e30a7c..6d72e43b87b 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -8,7 +8,7 @@ cd "${MY_DIR}/.." || exit 1 # Source constants and useful functions . bin/functions.sh -is_cmd pnpm || npm install pnpm -g +ensure_pnpm # Is node installed? @@ -36,10 +36,10 @@ fi log "Installing dependencies..." if [ -z "${ETHERPAD_PRODUCTION}" ]; then log "Installing dev dependencies with pnpm" - pnpm --recursive i || exit 1 + run_pnpm --recursive i || exit 1 else log "Installing production dependencies with pnpm" - pnpm --recursive i --production || exit 1 + run_pnpm --recursive i --production || exit 1 fi # Remove all minified data to force node creating it new diff --git a/bin/run.sh b/bin/run.sh index 3f6b119bc48..8ac09211275 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -35,8 +35,8 @@ if [ -z "$NODE_ENV" ] || [ "$NODE_ENV" = "development" ]; then ADMIN_UI_PATH="$(dirname "$0")/../admin" UI_PATH="$(dirname "$0")/../ui" log "Creating the admin UI..." - (cd "$ADMIN_UI_PATH" && pnpm run build) - (cd "$UI_PATH" && pnpm run build) + (cd "$ADMIN_UI_PATH" && run_pnpm run build) + (cd "$UI_PATH" && run_pnpm run build) else log "Cannot create the admin UI in production mode" fi @@ -45,4 +45,4 @@ fi log "Starting Etherpad..." # cd src -exec pnpm run prod "$@" +exec_pnpm run prod "$@" diff --git a/src/node/hooks/express/updateActions.ts b/src/node/hooks/express/updateActions.ts index 2962f5afab9..2383c9afe65 100644 --- a/src/node/hooks/express/updateActions.ts +++ b/src/node/hooks/express/updateActions.ts @@ -18,6 +18,7 @@ import {tailLines, appendLine} from '../../updater/updateLog'; import {performRollback} from '../../updater/RollbackHandler'; import {UpdateState} from '../../updater/types'; import {isValidTag} from '../../updater/refSafety'; +import {defaultPnpmCommand, resolvePnpmCommandSync} from '../../updater/pnpm'; import {applyUpdate} from '../../updater/applyPipeline'; import {cancelScheduler} from '../../updater'; import {getIo} from './socketio'; @@ -83,11 +84,7 @@ const buildPreflightDeps = (installMethod: ReturnType new Promise((resolve) => { - const c = spawn('pnpm', ['--version'], {stdio: 'ignore'}); - c.on('close', (code) => resolve(code === 0)); - c.on('error', () => resolve(false)); - }), + pnpmOnPath: async () => resolvePnpmCommandSync(settings.root) != null, // We just acquired the lock in the apply endpoint, so don't double-check it here. lockHeld: async () => false, remoteHasTag: (tag: string) => new Promise((resolve) => { @@ -172,6 +169,7 @@ export const expressCreateServer = ( const targetTag = state.latest.tag; let responded = false; + const pnpmCommand = resolvePnpmCommandSync(settings.root) ?? defaultPnpmCommand; try { const result = await applyUpdate({ @@ -232,6 +230,7 @@ export const expressCreateServer = ( targetTag: tag, now: () => new Date(), exit: (code: number) => process.exit(code), + pnpmCommand, }), performRollback: (s) => performRollback(s, getRollbackDeps()), appendLog: (line) => appendLine(logPath(), line), diff --git a/src/node/updater/RollbackHandler.ts b/src/node/updater/RollbackHandler.ts index e90e8b7fd15..15e512fc16e 100644 --- a/src/node/updater/RollbackHandler.ts +++ b/src/node/updater/RollbackHandler.ts @@ -3,6 +3,7 @@ import log4js from 'log4js'; import {UpdateState} from './types'; import type {SpawnFn} from './UpdateExecutor'; import {appendLine} from './updateLog'; +import {pnpmInvocation, PnpmCommand} from './pnpm'; const logger = log4js.getLogger('updater'); @@ -18,6 +19,8 @@ export interface RollbackDeps { now: () => Date; /** Health-check window after a fresh boot. Default 60s; set via updates.rollbackHealthCheckSeconds. */ rollbackHealthCheckSeconds: number; + /** Production callers may override this to run pnpm via Corepack. */ + pnpmCommand?: PnpmCommand; } const runStep = ( @@ -26,6 +29,7 @@ const runStep = ( logPath: string, cmd: string, args: string[], + tag = `${cmd} ${args.join(' ')}`, ): Promise => new Promise((resolve) => { let settled = false; const settle = (c: number | null) => { @@ -34,7 +38,6 @@ const runStep = ( resolve(c); }; const child = spawnFn(cmd, args, {cwd, stdio: ['ignore', 'pipe', 'pipe']}); - const tag = `${cmd} ${args.join(' ')}`; child.stdout.on('data', (b: Buffer) => { const t = b.toString().trimEnd(); logger.info(`[rollback ${tag}] ${t}`); @@ -129,7 +132,10 @@ export const performRollback = async (state: UpdateState, deps: RollbackDeps): P } } - const installCode = await runStep(deps.spawnFn, deps.repoDir, logPath, 'pnpm', ['install', '--frozen-lockfile']); + const pnpmInstall = pnpmInvocation(deps.pnpmCommand, ['install', '--frozen-lockfile']); + const installCode = await runStep( + deps.spawnFn, deps.repoDir, logPath, + pnpmInstall.command, pnpmInstall.args, pnpmInstall.label); if (installCode !== 0) return failTerminal(`pnpm install exit ${installCode}`); const at = deps.now().toISOString(); diff --git a/src/node/updater/UpdateExecutor.ts b/src/node/updater/UpdateExecutor.ts index 07881065e46..ec707e1500f 100644 --- a/src/node/updater/UpdateExecutor.ts +++ b/src/node/updater/UpdateExecutor.ts @@ -4,6 +4,7 @@ import {SpawnOptions} from 'node:child_process'; import {UpdateState} from './types'; import {appendLine} from './updateLog'; import {assertValidTag, refsTagsForm} from './refSafety'; +import {pnpmInvocation, PnpmCommand} from './pnpm'; const logger = log4js.getLogger('updater'); @@ -39,6 +40,8 @@ export interface ExecutorDeps { now: () => Date; /** process.exit injection so tests can assert exit code without actually exiting. */ exit: (code: number) => void; + /** Production callers may override this to run pnpm via Corepack. */ + pnpmCommand?: PnpmCommand; } export type ExecutorResult = @@ -53,6 +56,7 @@ const runStep = ( logPath: string, cmd: string, args: string[], + tag = `${cmd} ${args.join(' ')}`, ): Promise<{code: number | null; stderr: string}> => new Promise((resolve) => { let stderr = ''; let settled = false; @@ -62,7 +66,6 @@ const runStep = ( resolve(v); }; const child = spawnFn(cmd, args, {cwd: repoDir, stdio: ['ignore', 'pipe', 'pipe']}); - const tag = `${cmd} ${args.join(' ')}`; child.stdout.on('data', (chunk: Buffer) => { const txt = chunk.toString().trimEnd(); logger.info(`[${tag}] ${txt}`); @@ -166,10 +169,16 @@ export const executeUpdate = async (deps: ExecutorDeps): Promise deps.spawnFn, deps.repoDir, logPath, 'git', ['checkout', refsTagsForm(safeTag)]); if (r.code !== 0) return fail('failed-checkout', `git checkout exit ${r.code}: ${r.stderr.trim()}`); - r = await runStep(deps.spawnFn, deps.repoDir, logPath, 'pnpm', ['install', '--frozen-lockfile']); + const pnpmInstall = pnpmInvocation(deps.pnpmCommand, ['install', '--frozen-lockfile']); + r = await runStep( + deps.spawnFn, deps.repoDir, logPath, + pnpmInstall.command, pnpmInstall.args, pnpmInstall.label); if (r.code !== 0) return fail('failed-install', `pnpm install exit ${r.code}: ${r.stderr.trim()}`); - r = await runStep(deps.spawnFn, deps.repoDir, logPath, 'pnpm', ['run', 'build:ui']); + const pnpmBuild = pnpmInvocation(deps.pnpmCommand, ['run', 'build:ui']); + r = await runStep( + deps.spawnFn, deps.repoDir, logPath, + pnpmBuild.command, pnpmBuild.args, pnpmBuild.label); if (r.code !== 0) return fail('failed-build', `pnpm run build:ui exit ${r.code}: ${r.stderr.trim()}`); // pending-verification: the next boot's RollbackHandler arms the health-check timer. diff --git a/src/node/updater/index.ts b/src/node/updater/index.ts index 99690c769b0..bbcfd87b2a4 100644 --- a/src/node/updater/index.ts +++ b/src/node/updater/index.ts @@ -20,6 +20,7 @@ import {createDrainer} from './SessionDrainer'; import {appendLine} from './updateLog'; import {isValidTag} from './refSafety'; import {InstallMethod, UpdateState} from './types'; +import {defaultPnpmCommand, resolvePnpmCommandSync} from './pnpm'; const logger = log4js.getLogger('updater'); @@ -190,6 +191,7 @@ export const getRollbackDeps = (): RollbackDeps => ({ exit: (code: number) => process.exit(code), now: () => new Date(), rollbackHealthCheckSeconds: Number(settings.updates.rollbackHealthCheckSeconds) || 60, + pnpmCommand: resolvePnpmCommandSync(settings.root) ?? defaultPnpmCommand, }); const lockPath = (): string => path.join(settings.root, 'var', 'update.lock'); @@ -236,11 +238,7 @@ const buildSchedulerApplyDeps = (): ApplyPipelineDeps => ({ return Number.POSITIVE_INFINITY; } }, - pnpmOnPath: () => new Promise((resolve) => { - const c = spawn('pnpm', ['--version'], {stdio: 'ignore'}); - c.on('close', (code) => resolve(code === 0)); - c.on('error', () => resolve(false)); - }), + pnpmOnPath: async () => resolvePnpmCommandSync(settings.root) != null, lockHeld: async () => false, // pipeline already holds the lock here remoteHasTag: (tagName: string) => new Promise((resolve) => { const c = spawn('git', ['ls-remote', '--tags', 'origin', tagName], @@ -281,6 +279,7 @@ const buildSchedulerApplyDeps = (): ApplyPipelineDeps => ({ targetTag, now: () => new Date(), exit: (code: number) => process.exit(code), + pnpmCommand: resolvePnpmCommandSync(settings.root) ?? defaultPnpmCommand, }), performRollback: (s) => performRollback(s, getRollbackDeps()), appendLog: (line: string) => appendLine(logPath(), line), diff --git a/src/node/updater/pnpm.ts b/src/node/updater/pnpm.ts new file mode 100644 index 00000000000..70c2c2a6d2d --- /dev/null +++ b/src/node/updater/pnpm.ts @@ -0,0 +1,23 @@ +import {spawnSync} from 'node:child_process'; + +export type PnpmCommand = readonly [string, ...string[]]; + +export const defaultPnpmCommand: PnpmCommand = ['pnpm']; + +export const resolvePnpmCommandSync = (cwd: string): PnpmCommand | null => { + for (const cmd of [defaultPnpmCommand, ['corepack', 'pnpm'] as const]) { + const [command, ...args] = cmd; + const result = spawnSync(command, [...args, '--version'], {cwd, stdio: 'ignore'}); + if (result.status === 0) return cmd; + } + return null; +}; + +export const pnpmInvocation = (pnpmCommand: PnpmCommand | undefined, args: string[]) => { + const [command, ...prefix] = pnpmCommand ?? defaultPnpmCommand; + return { + command, + args: [...prefix, ...args], + label: `pnpm ${args.join(' ')}`, + }; +}; From 88e8335bdc17ba26c9f1f6e3122bad96d553c993 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 14 May 2026 16:55:24 +0100 Subject: [PATCH 4/6] Inline AuthorPage admin helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- admin/src/components/SearchField.tsx | 15 --------------- admin/src/pages/AuthorPage.tsx | 20 ++++++++++++++------ admin/src/utils/sorting.ts | 6 ------ 3 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 admin/src/components/SearchField.tsx delete mode 100644 admin/src/utils/sorting.ts diff --git a/admin/src/components/SearchField.tsx b/admin/src/components/SearchField.tsx deleted file mode 100644 index e6fb9cde9de..00000000000 --- a/admin/src/components/SearchField.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type {ChangeEventHandler, FC} from 'react'; -import {Search} from 'lucide-react'; - -export type SearchFieldProps = { - value: string; - onChange: ChangeEventHandler; - placeholder?: string; -}; - -export const SearchField: FC = ({onChange, value, placeholder}) => ( - - - - -); diff --git a/admin/src/pages/AuthorPage.tsx b/admin/src/pages/AuthorPage.tsx index ea119e63334..c9749092192 100644 --- a/admin/src/pages/AuthorPage.tsx +++ b/admin/src/pages/AuthorPage.tsx @@ -1,18 +1,23 @@ import {Trans, useTranslation} from "react-i18next"; import {useEffect, useMemo, useState} from "react"; import * as Dialog from "@radix-ui/react-dialog"; -import {ChevronLeft, ChevronRight, Trash2} from "lucide-react"; +import {ChevronLeft, ChevronRight, Search, Trash2} from "lucide-react"; import {useStore} from "../store/store.ts"; -import {SearchField} from "../components/SearchField.tsx"; import {ColorSwatch} from "../components/ColorSwatch.tsx"; import {IconButton} from "../components/IconButton.tsx"; -import {determineSorting} from "../utils/sorting.ts"; import {useDebounce} from "../utils/useDebounce.ts"; import { AnonymizePreview, AnonymizeResult, AuthorRow, AuthorSearchQuery, AuthorSearchResult, AuthorSortBy, } from "../utils/AuthorSearch.ts"; +const determineSorting = (sortBy: string, ascending: boolean, currentSymbol: string) => { + if (sortBy === currentSymbol) { + return ascending ? 'sort up' : 'sort down'; + } + return 'sort none'; +}; + type DialogState = | {phase: 'closed'} | {phase: 'loading-preview', authorID: string, name: string | null} @@ -203,9 +208,12 @@ export const AuthorPage = () => { - setSearchTerm(v.target.value)} - placeholder={t('ep_admin_authors:search-placeholder')}/> + + setSearchTerm(e.target.value)} + placeholder={t('ep_admin_authors:search-placeholder')}/> + +