diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml deleted file mode 100644 index 1e32877..0000000 --- a/.github/workflows/backend-tests.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: "Backend tests" - -# any branch is useful for testing before a PR is submitted -on: [push, pull_request] - -jobs: - withplugins: - # run on pushes to any branch - # run on PRs from external forks - if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) - name: with Plugins - runs-on: ubuntu-latest - - steps: - - - name: Install etherpad core - uses: actions/checkout@v6 - with: - repository: ether/etherpad-lite - path: ./etherpad - - - name: Checkout plugin repository - uses: actions/checkout@v6 - with: - path: ./plugin - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v5 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Prepare Etherpad core - run: | - cd ./etherpad - ./bin/installDeps.sh - pnpm link --global - - - name: Prepare plugin - run: | - pnpm config set auto-install-peers false - cd ./plugin - PLUGIN_NAME=$(npx -c 'printf %s\\n "${npm_package_name}"') || exit 1 - echo "PLUGIN_NAME=$PLUGIN_NAME" >> $GITHUB_ENV - pnpm install - pnpm build - pnpm link --global - - name: Link plugin to Etherpad - run: | - cd ./etherpad/src - pnpm link "$PLUGIN_NAME" - - - name: Remove core tests so only the client is tested - run: rm -rf ./etherpad/src/tests/backend/specs - - - name: Run the backend tests - run: | - cd ./etherpad/src - pnpm --filter ep_etherpad-lite exec cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts ../node_modules/ep_*/static/tests/backend/specs/** ../node_modules/$PLUGIN_NAME/static/tests/backend/specs/** diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index cd0410f..d228377 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -10,57 +10,35 @@ on: - main - master +env: + PNPM_HOME: ~/.pnpm-store + jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - repository: ether/etherpad-lite - path: etherpad-lite - - - run: mv etherpad-lite .. - - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - uses: actions/cache@v5 - name: Setup pnpm cache + name: Cache pnpm store with: - path: ${{ env.STORE_PATH }} + path: ${{ env.PNPM_HOME }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false - - run: cd ../etherpad-lite && ./bin/installDeps.sh && pnpm link --global + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - - run: | - pnpm config set auto-install-peers false - pnpm i + run: pnpm i - - run: | - has_testcli_script () { - [[ $(pnpm run | grep "^ test" | wc -l) > 0 ]] - } - - if has_testcli_script; then - pnpm run test - else - echo "No test script found" - fi - name: Run tests if available + run: pnpm run build - run: pnpm run lint @@ -73,29 +51,23 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: https://registry.npmjs.org/ - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - version: 10 - run_install: false - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - uses: actions/cache@v5 - name: Setup pnpm cache + name: Cache pnpm store with: - path: ${{ env.STORE_PATH }} + path: ${{ env.PNPM_HOME }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - - name: Only install direct dependencies - run: pnpm config set auto-install-peers false + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + - + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + registry-url: https://registry.npmjs.org/ - name: Bump version (patch) run: | diff --git a/.npmrc b/.npmrc deleted file mode 100644 index f301fed..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers=false diff --git a/package.json b/package.json index 7bc44bc..c1cfa2d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "etherpad-cli-client", "description": "Node Client for Etherpad", - "version": "4.0.2", + "version": "4.0.3", "type": "module", + "packageManager": "pnpm@11.1.2", "author": { "name": "John McLear", "email": "john@mclear.co.uk", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1743b7c..61be387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: false + autoInstallPeers: true excludeLinksFromLockfile: false importers: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d7265d2..01c8439 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ onlyBuiltDependencies: - unrs-resolver +strictDepBuilds: false diff --git a/src/index.ts b/src/index.ts index e736d11..0af2162 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export type PadClient = EventEmitter & { type ClientVarsMessage = { type: 'CLIENT_VARS'; data: { + userId?: string; collab_client_vars: { initialAttributedText: AText; apool: JsonableAttributePool; @@ -108,6 +109,7 @@ const isDisconnectMessage = (value: unknown): value is DisconnectMessage => export const connect = (host?: string): PadClient => { const ee = new EventEmitter() as PadClient; + let authorId: string | null = null; const padState: PadState = { host: '', path: '', @@ -198,6 +200,7 @@ export const connect = (host?: string): PadClient => { padState.atext = obj.data.collab_client_vars.initialAttributedText; padState.apool = new AttributePool().fromJsonable(obj.data.collab_client_vars.apool); padState.baseRev = obj.data.collab_client_vars.rev; + if (typeof obj.data.userId === 'string') authorId = obj.data.userId; ee.emit('connected', padState); } else if (isNewChangesMessage(obj)) { if (obj.data.newRev <= padState.baseRev) return; @@ -240,16 +243,31 @@ export const connect = (host?: string): PadClient => { }; ee.append = (text: string) => { - const newChangeset = Changeset.makeSplice( - padState.atext.text, padState.atext.text.length, 0, text); + // Insert just before the trailing '\n' so the pad's "doc always ends + // with \n" invariant is preserved. Etherpad's server (post-2.7.x) + // rejects USER_CHANGES whose application would leave the doc without + // a trailing newline, and tags inserts with no `author` attribute as + // bad changesets — both produced silent disconnects with the previous + // append-at-text.length / no-attribs behaviour. + const insertPos = Math.max(0, padState.atext.text.length - 1); + const attribs: Array<[string, string]> | undefined = + authorId ? [['author', authorId]] : undefined; + const localChangeset = Changeset.makeSplice( + padState.atext.text, insertPos, 0, text, attribs, padState.apool); const newRev = padState.baseRev; - padState.atext = Changeset.applyToAText(newChangeset, padState.atext, padState.apool) as AText; + padState.atext = Changeset.applyToAText( + localChangeset, padState.atext, padState.apool) as AText; + // Build a minimal wire pool containing only the attributes referenced + // by this changeset so the server can resolve our `*N` slot numbers. + const wireApool = new AttributePool(); + const wireChangeset = Changeset.moveOpsToNewPool( + localChangeset, padState.apool, wireApool); const msg: PendingMessage = { component: 'pad', type: 'USER_CHANGES', baseRev: newRev, - changeset: newChangeset, - apool: new AttributePool().toJsonable(), + changeset: wireChangeset, + apool: wireApool.toJsonable(), }; sendMessage(msg); };