diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index a5110130824..c5528bc364c 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -32,15 +32,15 @@ jobs: timeout-minutes: 120 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - name: Set up JDK 25 + - uses: actions/checkout@v7 + - name: Set up JDK 11 uses: actions/setup-java@v5 with: java-version: 25 distribution: temurin cache: 'maven' - name: Set up Node.js 22 - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 22 cache: 'npm' @@ -50,6 +50,11 @@ jobs: - name: Install website npm dependencies working-directory: zookeeper-website run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('zookeeper-website/package-lock.json') }} - name: Install Playwright browsers and system dependencies working-directory: zookeeper-website run: npx playwright install --with-deps diff --git a/NOTICE.txt b/NOTICE.txt index 9b3afcb4705..3acf69dda59 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -18,7 +18,6 @@ These BSD licensed files: ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_itr.c ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_itr.h ./zookeeper-client/zookeeper-client-c/src/hashtable/hashtable_private.h - ./zookeeper-docs/src/main/resources/markdown/skin/prototype.js This Apache 2.0 licensed file: ./zookeeper-contrib/zookeeper-contrib-zooinspector/src/main/java/com/nitido/utils/toaster/Toaster.java diff --git a/conf/zoo_sample.cfg b/conf/zoo_sample.cfg index d0db6e99a1d..c70eb842d69 100644 --- a/conf/zoo_sample.cfg +++ b/conf/zoo_sample.cfg @@ -19,7 +19,7 @@ clientPort=2181 # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # -# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance +# https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/administration#maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 diff --git a/pom.xml b/pom.xml index 1aacd272b04..5a1b5022424 100644 --- a/pom.xml +++ b/pom.xml @@ -1094,9 +1094,6 @@ .travis.yml excludeFindBugsFilter.xml README_packaging.md - src/main/resources/markdown/skin/* - src/main/resources/markdown/html/* - src/main/resources/markdown/images/* **/src/test/resources/embedded/*.conf **/JMX-RESOURCES diff --git a/zookeeper-client/zookeeper-client-c/README b/zookeeper-client/zookeeper-client-c/README index 24869756b63..a31c6aa060b 100644 --- a/zookeeper-client/zookeeper-client-c/README +++ b/zookeeper-client/zookeeper-client-c/README @@ -8,7 +8,8 @@ For the latest information about ZooKeeper, please visit our website at: and our wiki, at: https://cwiki.apache.org/confluence/display/ZOOKEEPER -Full documentation for this release can also be found in ../../docs/index.html +Full documentation for this release can also be found at: + https://zookeeper.apache.org/doc/current/ OVERVIEW @@ -34,8 +35,9 @@ Sync and Async API. INSTALLATION -Please refer to the "Installation" item under "C Binding" section in file -".../trunk/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md" +Please refer to the "Installation" item under "C Binding" in the +Programmer's Guide: + https://zookeeper.apache.org/doc/current/developer/programmers-guide/bindings#installation EXAMPLE/SAMPLE C CLIENT SHELL diff --git a/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako b/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako index 07c91c331bc..814676257c3 100644 --- a/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako +++ b/zookeeper-contrib/zookeeper-contrib-huebrowser/zkui/src/zkui/templates/tree.mako @@ -69,7 +69,7 @@ ${shared.header("ZooKeeper Browser > Tree > %s > %s" % (cluster['nice_name'], pa
-Details on stat information. +Details on stat information. ${shared.footer()} diff --git a/zookeeper-contrib/zookeeper-contrib-monitoring/README b/zookeeper-contrib/zookeeper-contrib-monitoring/README index 81521fcb0e2..44f7b2cefc8 100644 --- a/zookeeper-contrib/zookeeper-contrib-monitoring/README +++ b/zookeeper-contrib/zookeeper-contrib-monitoring/README @@ -81,5 +81,5 @@ Apache License 2.0 or later. ZooKeeper 4letterwords Commands ------------------------------- -http://zookeeper.apache.org/docs/current/zookeeperAdmin.html#sc_zkCommands +https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/commands#the-four-letter-words diff --git a/zookeeper-recipes/README.txt b/zookeeper-recipes/README.txt index bc788741d63..db41fa0d463 100644 --- a/zookeeper-recipes/README.txt +++ b/zookeeper-recipes/README.txt @@ -13,8 +13,9 @@ some unit testing with both the c and java recipe code. zkr_recipe-name_methodname (eg. zkr_lock_lock in zookeeper-recipes-lock/src/c) -6) The various recipes are in ../docs/recipes.html or -../../docs/reciped.pdf. Also, this is not an exhaustive list by any chance. +6) The various recipes are documented at + https://zookeeper.apache.org/doc/current/developer/recipes + Also, this is not an exhaustive list by any chance. Zookeeper is used (and can be used) for more than what we have listed in the docs. 7) To run the c tests in all the recipes, diff --git a/zookeeper-recipes/zookeeper-recipes-election/README.txt b/zookeeper-recipes/zookeeper-recipes-election/README.txt index f854b275eb8..3de76293a30 100644 --- a/zookeeper-recipes/zookeeper-recipes-election/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-election/README.txt @@ -16,7 +16,7 @@ --> 1) This election interface recipe implements the leader election recipe -mentioned in ../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes 2) To compile the leader election java recipe you can just run ant jar from this directory. diff --git a/zookeeper-recipes/zookeeper-recipes-lock/README.txt b/zookeeper-recipes/zookeeper-recipes-lock/README.txt index 438de1bdbb9..e4c63081ab1 100644 --- a/zookeeper-recipes/zookeeper-recipes-lock/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-lock/README.txt @@ -16,7 +16,7 @@ --> 1) This lock interface recipe implements the lock recipe -mentioned in ../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes 2) To compile the lock java recipe you can just run ant jar from this directory. For compiling the c library go to zookeeper-client/zookeeper-client-c and read diff --git a/zookeeper-recipes/zookeeper-recipes-queue/README.txt b/zookeeper-recipes/zookeeper-recipes-queue/README.txt index d59a3c3170a..49fb6f546d5 100644 --- a/zookeeper-recipes/zookeeper-recipes-queue/README.txt +++ b/zookeeper-recipes/zookeeper-recipes-queue/README.txt @@ -16,7 +16,7 @@ --> 1) This queue interface recipe implements the queue recipe -mentioned in ../../../docs/recipes.[html,pdf]. +documented at https://zookeeper.apache.org/doc/current/developer/recipes A more detailed explanation is at http://www.cloudera.com/blog/2009/05/28/building-a-distributed-concurrent-queue-with-apache-zookeeper/ 2) This recipe does not handle KeeperException.ConnectionLossException or ZCONNECTIONLOSS. It will only work correctly once ZOOKEEPER-22 https://issues.apache.org/jira/browse/ZOOKEEPER-22 is resolved. diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java index 32893d46af4..239f97b0f70 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java @@ -1718,7 +1718,7 @@ public void delete(final String path, int version) throws InterruptedException, * Note: The maximum allowable size of all of the data arrays in all of * the setData operations in this single request is typically 1 MB * (1,048,576 bytes). This limit is specified on the server via - * jute.maxbuffer. + * jute.maxbuffer. * Requests larger than this will cause a KeeperException to be * thrown. * diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java index cf252b9b08a..92059bef5ce 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java @@ -37,7 +37,7 @@ * See the "Pluggable ZooKeeper authentication" section of the * "Zookeeper Programmer's Guide" for general details of implementing an * authentication plugin. e.g. - * http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_ZooKeeperPluggableAuthentication + * https://zookeeper.apache.org/doc/current/developer/programmers-guide/pluggable-authentication * * This class looks for a numeric "key" under the /key node. * Authorization is granted if the user passes in as authorization a number diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java index 3c615f4faf7..6566cdaf3c6 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/command/FourLetterCommands.java @@ -33,108 +33,108 @@ public class FourLetterCommands { /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int confCmd = ByteBuffer.wrap("conf".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int consCmd = ByteBuffer.wrap("cons".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int crstCmd = ByteBuffer.wrap("crst".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int dirsCmd = ByteBuffer.wrap("dirs".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int dumpCmd = ByteBuffer.wrap("dump".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int enviCmd = ByteBuffer.wrap("envi".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int getTraceMaskCmd = ByteBuffer.wrap("gtmk".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int ruokCmd = ByteBuffer.wrap("ruok".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int setTraceMaskCmd = ByteBuffer.wrap("stmk".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int srvrCmd = ByteBuffer.wrap("srvr".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int srstCmd = ByteBuffer.wrap("srst".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int statCmd = ByteBuffer.wrap("stat".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchcCmd = ByteBuffer.wrap("wchc".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchpCmd = ByteBuffer.wrap("wchp".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int wchsCmd = ByteBuffer.wrap("wchs".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int mntrCmd = ByteBuffer.wrap("mntr".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ public static final int isroCmd = ByteBuffer.wrap("isro".getBytes()).getInt(); /* - * See + * See * Zk Admin. this link is for all the commands. */ protected static final int hashCmd = ByteBuffer.wrap("hash".getBytes()).getInt(); diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java index c126ea2e290..86b3b8a7c6a 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java @@ -52,8 +52,8 @@ * configuration information. This file is a Properties file, so keys and * values are separated by equals (=) and the key/value pairs are separated * by new lines. The following is a general summary of keys used in the - * configuration file. For full details on this see the documentation in - * docs/index.html + * configuration file. For full details see the + * Administrator's Guide. *
    *
  1. dataDir - The directory where the ZooKeeper data is stored.
  2. *
  3. dataLogDir - The directory where the ZooKeeper transaction log is stored.
  4. diff --git a/zookeeper-website/.gitignore b/zookeeper-website/.gitignore index 583284939ff..66bf0a90734 100644 --- a/zookeeper-website/.gitignore +++ b/zookeeper-website/.gitignore @@ -41,3 +41,9 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ + +# Generated developer list (see scripts/extract-developers.js) +/app/pages/_landing/credits/developers.json + +# Generated sitemap (see scripts/generate-sitemap.ts) +/public/sitemap.xml diff --git a/zookeeper-website/app/components/docs/archive-docs-banner.tsx b/zookeeper-website/app/components/docs/archive-docs-banner.tsx deleted file mode 100644 index ba78c0bbd44..00000000000 --- a/zookeeper-website/app/components/docs/archive-docs-banner.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { ArrowRight, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { CURRENT_VERSION } from "@/lib/current-version"; -import { DOCS_ARCHIVE_SNAPSHOT_ENV } from "@/lib/docs-archive"; -import { Button } from "@/ui/button"; - -const bannerStorageKey = `zookeeper-docs-archive-banner-dismissed:${CURRENT_VERSION}`; - -export function ArchiveDocsBanner() { - // Set only by `build:docs-archive`, never by the current docs build or dev. - const isArchiveDocsView = import.meta.env[DOCS_ARCHIVE_SNAPSHOT_ENV] === "1"; - const [visible, setVisible] = useState(false); - - useEffect(() => { - if (!isArchiveDocsView) { - return; - } - - setVisible(localStorage.getItem(bannerStorageKey) !== "1"); - }, [isArchiveDocsView]); - - if (!isArchiveDocsView || !visible) { - return null; - } - - const dismiss = () => { - localStorage.setItem(bannerStorageKey, "1"); - setVisible(false); - }; - - return ( -
    -
    -

    - You are viewing the v{CURRENT_VERSION} docs.{" "} - - Switch to latest - - -

    - -
    -
    - ); -} diff --git a/zookeeper-website/app/components/getting-started.tsx b/zookeeper-website/app/components/getting-started.tsx index 633c8b7c58a..fae43f7d4ad 100644 --- a/zookeeper-website/app/components/getting-started.tsx +++ b/zookeeper-website/app/components/getting-started.tsx @@ -35,7 +35,7 @@ export function GettingStartedSection() { { title: "3. Write a Client", desc: "Follow the basic tutorial to implement distributed primitives like barriers and queues.", - to: `${CURRENT_DOCS_PATH}/developer/basic-tutorial` + to: "https://cwiki.apache.org/confluence/display/ZOOKEEPER/Tutorial" } ]; return ( diff --git a/zookeeper-website/app/components/links.ts b/zookeeper-website/app/components/links.ts index 6f7ca909ef8..97fa3d8be3a 100644 --- a/zookeeper-website/app/components/links.ts +++ b/zookeeper-website/app/components/links.ts @@ -16,8 +16,7 @@ // limitations under the License. // -import { CURRENT_VERSION } from "@/lib/current-version"; -import { CURRENT_DOCS_PATH } from "@/lib/docs-paths"; +import { getReleasedDocUrl, LTS_VERSIONS } from "@/lib/released-docs-versions"; interface LinkType { label: string; @@ -62,10 +61,12 @@ export const projectLinks: LinkType[] = [ ]; export const documentationLinks: (LinkType | NestedLinkType)[] = [ - { - label: `${CURRENT_VERSION} Documentation`, - to: CURRENT_DOCS_PATH - }, + ...LTS_VERSIONS.map( + (version): LinkType => ({ + label: `${version} Documentation`, + to: getReleasedDocUrl(version) + }) + ), { label: "Issue Tracking", to: "https://issues.apache.org/jira/browse/ZOOKEEPER", @@ -88,8 +89,8 @@ export const documentationLinks: (LinkType | NestedLinkType)[] = [ external: true }, { - label: "IRC Channel", - to: "/irc" + label: "Slack Channel", + to: "/slack" } ] } diff --git a/zookeeper-website/app/lib/current-version.ts b/zookeeper-website/app/lib/current-version.ts index d02009316e6..c1c6461e016 100644 --- a/zookeeper-website/app/lib/current-version.ts +++ b/zookeeper-website/app/lib/current-version.ts @@ -16,4 +16,4 @@ // limitations under the License. // -export const CURRENT_VERSION = "3.9.5"; +export const CURRENT_VERSION = "3.10.0"; diff --git a/zookeeper-website/app/lib/docs-archive.ts b/zookeeper-website/app/lib/docs-archive.ts index 625ca8d0726..6744e37300e 100644 --- a/zookeeper-website/app/lib/docs-archive.ts +++ b/zookeeper-website/app/lib/docs-archive.ts @@ -52,7 +52,3 @@ export function getBuildTarget(): "landing" | undefined { ? "landing" : undefined; } - -// Marks a docs build as an archived snapshot of an older release. The VITE_ -// prefix intentionally exposes it to bundled browser code via import.meta.env. -export const DOCS_ARCHIVE_SNAPSHOT_ENV = "VITE_ZOOKEEPER_DOCS_ARCHIVE_SNAPSHOT"; diff --git a/zookeeper-website/app/lib/released-docs-versions.ts b/zookeeper-website/app/lib/released-docs-versions.ts index 382953df96c..53a901005dc 100644 --- a/zookeeper-website/app/lib/released-docs-versions.ts +++ b/zookeeper-website/app/lib/released-docs-versions.ts @@ -16,6 +16,8 @@ // limitations under the License. // +import { CURRENT_VERSION } from "./current-version"; + type PreRelease = "alpha" | "beta" | "stable"; interface ParsedVersion { @@ -47,7 +49,7 @@ const preReleaseOrder: Record = { alpha: 0 }; -export const LEGACY_RELEASED_DOC_VERSIONS = new Set([ +const RAW_RELEASED_DOC_VERSIONS_LIST = [ "3.1.2", "3.2.2", "3.3.2", @@ -99,16 +101,22 @@ export const LEGACY_RELEASED_DOC_VERSIONS = new Set([ "3.9.1", "3.9.2", "3.9.3", - "3.9.4" -]); + "3.9.4", + "3.9.5", + CURRENT_VERSION +] as const; -export const REACT_ROUTER_RELEASED_DOC_VERSIONS = new Set(); +export type ReleasedDocVersion = + (typeof RAW_RELEASED_DOC_VERSIONS_LIST)[number]; + +export const RAW_RELEASED_DOC_VERSIONS = new Set( + RAW_RELEASED_DOC_VERSIONS_LIST +); -export const RAW_RELEASED_DOC_VERSIONS: string[] = [ - ...new Set([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]) +export const LTS_VERSIONS: ReleasedDocVersion[] = [ + CURRENT_VERSION, + "3.9.5", + "3.8.6" ]; export function sortVersionsDesc(versions: string[]): string[] { @@ -126,36 +134,15 @@ export function sortVersionsDesc(versions: string[]): string[] { * All released documentation versions available under /doc/. * Maintained manually because archived docs live in the asf-site branch. */ -export const RELEASED_DOC_VERSIONS: string[] = sortVersionsDesc( - RAW_RELEASED_DOC_VERSIONS -); +export const RELEASED_DOC_VERSIONS: string[] = sortVersionsDesc([ + ...RAW_RELEASED_DOC_VERSIONS +]); export function getReleasedDocUrl(version: string): string { - const basePath = `/doc/r${version}`; - return LEGACY_RELEASED_DOC_VERSIONS.has(version) - ? `${basePath}/index.html` - : `${basePath}/`; + return `/doc/r${version}/`; } export function getReleasedDocVersions(): string[] { - if (typeof window !== "undefined") { - const override = window.localStorage.getItem( - "__released_doc_versions_override__" - ); - if (override) { - try { - const parsed = JSON.parse(override); - if ( - Array.isArray(parsed) && - parsed.every((value) => typeof value === "string") - ) { - return sortVersionsDesc(parsed); - } - } catch { - // Ignore invalid test overrides and fall back to build-time data. - } - } - } - - return RELEASED_DOC_VERSIONS; + const ltsSet = new Set(LTS_VERSIONS); + return RELEASED_DOC_VERSIONS.filter((v) => !ltsSet.has(v)); } diff --git a/zookeeper-website/app/pages/_docs/docs-layout.tsx b/zookeeper-website/app/pages/_docs/docs-layout.tsx index d809707cb5e..cadfd4b4a1e 100644 --- a/zookeeper-website/app/pages/_docs/docs-layout.tsx +++ b/zookeeper-website/app/pages/_docs/docs-layout.tsx @@ -18,7 +18,6 @@ import { RootProvider } from "fumadocs-ui/provider/react-router"; import { Outlet } from "react-router"; -import { ArchiveDocsBanner } from "@/components/docs/archive-docs-banner"; import { SearchDialog } from "@/components/docs/search/docs-search"; export default function DocsLayout() { @@ -28,7 +27,6 @@ export default function DocsLayout() { SearchDialog }} > - ); diff --git a/zookeeper-website/app/pages/_docs/docs/index.tsx b/zookeeper-website/app/pages/_docs/docs/index.tsx index f161a819f2e..256418539db 100644 --- a/zookeeper-website/app/pages/_docs/docs/index.tsx +++ b/zookeeper-website/app/pages/_docs/docs/index.tsx @@ -38,6 +38,7 @@ import { OlderDocsPicker } from "@/components/docs/older-docs-picker"; import type { MDXComponents } from "mdx/types"; import { getDocsBasePath, resolveDocsHref } from "@/lib/docs-paths"; import { SITE_URL } from "@/lib/site"; +import { CURRENT_VERSION } from "@/lib/current-version"; // Extend default MDX components to include shared UI blocks globally. // Note: We'll override the 'a' component in the renderer to handle route-specific logic @@ -66,7 +67,7 @@ export function baseOptions(): BaseLayoutProps { width={16} height={16} /> -

    Apache ZooKeeper

    +

    Apache ZooKeeper {CURRENT_VERSION}

    ) }, diff --git a/zookeeper-website/app/pages/_landing/credits/developers.json b/zookeeper-website/app/pages/_landing/credits/developers.json deleted file mode 100644 index f4a77670e8c..00000000000 --- a/zookeeper-website/app/pages/_landing/credits/developers.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "pmc": [ - { "id": "tdunning", "name": "Ted Dunning", "organization": "MapR Technologies", "timezone": "-8" }, - { "id": "camille", "name": "Camille Fournier", "organization": "RentTheRunway", "timezone": "-5" }, - { "id": "phunt", "name": "Patrick Hunt", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "fpj", "name": "Flavio Junqueira", "organization": "Confluent", "timezone": "+0" }, - { "id": "ivank", "name": "Ivan Kelly", "organization": "Midokura", "timezone": "+2" }, - { "id": "mahadev", "name": "Mahadev Konar", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "michim", "name": "Michi Mutsuzaki", "organization": "Nicira", "timezone": "-8" }, - { "id": "cnauroth", "name": "Chris Nauroth", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "breed", "name": "Benjamin Reed", "organization": "Facebook", "timezone": "-8" }, - { "id": "henry", "name": "Henry Robinson", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "rgs", "name": "Raul Gutierrez Segales", "organization": "Pinterest", "timezone": "-8" }, - { "id": "rakeshr", "name": "Rakesh Radhakrishnan", "organization": "Intel", "timezone": "+5:30" }, - { "id": "hanm", "name": "Michael Han", "organization": "Twitter", "timezone": "-8" }, - { "id": "andor", "name": "Andor Molnar", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "eolivelli", "name": "Enrico Olivelli", "organization": "Diennea", "timezone": "+1" }, - { "id": "symat", "name": "Mate Szalay-Beko", "organization": "Cloudera Inc.", "timezone": "+1" } - ], - "committers": [ - { "id": "camille", "name": "Camille Fournier", "organization": "RentTheRunway", "timezone": "-5" }, - { "id": "phunt", "name": "Patrick Hunt", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "fpj", "name": "Flavio Junqueira", "organization": "Confluent", "timezone": "+1" }, - { "id": "cnauroth", "name": "Chris Nauroth", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "mahadev", "name": "Mahadev Konar", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "gkesavan", "name": "Giridharan Kesavan", "organization": "Hortonworks Inc.", "timezone": "-8" }, - { "id": "akornev", "name": "Andrew Kornev", "organization": "", "timezone": "" }, - { "id": "michim", "name": "Michi Mutsuzaki", "organization": "Nicira", "timezone": "-8" }, - { "id": "breed", "name": "Benjamin Reed", "organization": "Facebook", "timezone": "-8" }, - { "id": "henry", "name": "Henry Robinson", "organization": "Cloudera Inc.", "timezone": "-8" }, - { "id": "shralex", "name": "Alex Shraer", "organization": "Apple", "timezone": "-8" }, - { "id": "thawan", "name": "Thawan Kooburat", "organization": "Facebook", "timezone": "-8" }, - { "id": "rakeshr", "name": "Rakesh Radhakrishnan", "organization": "Intel", "timezone": "+5:30" }, - { "id": "hdeng", "name": "Hongchao Deng", "organization": "CoreOS", "timezone": "-8" }, - { "id": "rgs", "name": "Raul Gutierrez Segales", "organization": "Pinterest", "timezone": "-8" }, - { "id": "hanm", "name": "Michael Han", "organization": "Twitter", "timezone": "-8" }, - { "id": "arshad", "name": "Mohammad Arshad", "organization": "Huawei", "timezone": "+5:30" }, - { "id": "afine", "name": "Abraham Fine", "organization": "IFTTT", "timezone": "-8" }, - { "id": "andor", "name": "Andor Molnar", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "fangmin", "name": "Fangmin Lyu", "organization": "Facebook", "timezone": "-8" }, - { "id": "eolivelli", "name": "Enrico Olivelli", "organization": "Diennea", "timezone": "+1" }, - { "id": "nkalmar", "name": "Norbert Kalmar", "organization": "Cloudera", "timezone": "+1" }, - { "id": "enixon", "name": "Brian Nixon", "organization": "Facebook", "timezone": "-8" }, - { "id": "symat", "name": "Mate Szalay-Beko", "organization": "Cloudera Inc.", "timezone": "+1" }, - { "id": "ddiederen", "name": "Damien Diederen", "organization": "Crosstwine Labs", "timezone": "+1" } - ] -} diff --git a/zookeeper-website/app/pages/_landing/credits/index.tsx b/zookeeper-website/app/pages/_landing/credits/index.tsx index 50d15ee9066..8ed130dc95f 100644 --- a/zookeeper-website/app/pages/_landing/credits/index.tsx +++ b/zookeeper-website/app/pages/_landing/credits/index.tsx @@ -16,21 +16,16 @@ // limitations under the License. // -import data from "./developers.json"; +import developers from "./developers.json"; -interface Member { +interface Developer { id: string; name: string; - organization: string; + email: string; timezone: string; } -interface Credits { - pmc: Member[]; - committers: Member[]; -} - -function MemberTable({ members }: { members: Member[] }) { +function DeveloperTable({ developers }: { developers: Developer[] }) { return (
    @@ -38,22 +33,20 @@ function MemberTable({ members }: { members: Member[] }) { - + - {members.map((member) => ( + {developers.map((developer) => ( - - - - + + + + ))} @@ -63,8 +56,6 @@ function MemberTable({ members }: { members: Member[] }) { } export function CreditsPage() { - const credits = data as Credits; - return (
    @@ -79,24 +70,15 @@ export function CreditsPage() {

    - PMC Members -

    - -

    - ZooKeeper's active PMC members are listed below. -

    - - - -

    - Committers + Developers

    - ZooKeeper's active committers are listed below. + ZooKeeper's developers, as listed in the project's parent{" "} + pom.xml, are shown below.

    - +

    Contributors diff --git a/zookeeper-website/app/pages/_landing/home/community.tsx b/zookeeper-website/app/pages/_landing/home/community.tsx index 32113878002..82cf9d74f48 100644 --- a/zookeeper-website/app/pages/_landing/home/community.tsx +++ b/zookeeper-website/app/pages/_landing/home/community.tsx @@ -40,7 +40,10 @@ export function CommunitySection() { Mailing Lists @@ -64,16 +67,17 @@ export function CommunitySection() { -
  5. +
  6. - IRC Channel + Slack Channel

    - Chat with the community on #zookeeper at irc.libera.chat. + Chat with the community on #zookeeper in the ASF Slack + workspace.

    diff --git a/zookeeper-website/app/pages/_landing/security/content.md b/zookeeper-website/app/pages/_landing/security/content.md index 2182c632619..66daa4a49d9 100644 --- a/zookeeper-website/app/pages/_landing/security/content.md +++ b/zookeeper-website/app/pages/_landing/security/content.md @@ -24,7 +24,7 @@ The ASF Security team maintains a page with a description of how vulnerabilities ## Security model -ZooKeeper is a coordination service intended for use inside a trusted network, not exposed directly to the Internet. The [Admin Guide](https://zookeeper.apache.org/doc/current/zookeeperAdmin.html) states it plainly: "A ZooKeeper ensemble is expected to operate in a trusted computing environment. It is thus recommended deploying ZooKeeper behind a firewall." A fresh ensemble ships with no transport encryption, no peer authentication, and world-readable/writable znodes. Hardening is a shared responsibility between the ZooKeeper project and the operator. +ZooKeeper is a coordination service intended for use inside a trusted network, not exposed directly to the Internet. The [Admin Guide](https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/) states it plainly: "A ZooKeeper ensemble is expected to operate in a trusted computing environment. It is thus recommended deploying ZooKeeper behind a firewall." A fresh ensemble ships with no transport encryption, no peer authentication, and world-readable/writable znodes. Hardening is a shared responsibility between the ZooKeeper project and the operator. ### Security is opt-in @@ -254,7 +254,7 @@ See the documentation for more details on correct cluster administration. **Credit:** This issue was identified by Földi Tamás and Eugene Koontz. -**References:** [ZOOKEEPER-1045](https://issues.apache.org/jira/browse/ZOOKEEPER-1045) · [Server-Server mutual authentication](https://cwiki.apache.org/confluence/display/ZOOKEEPER/Server-Server+mutual+authentication) · [ZooKeeper Admin Guide](https://zookeeper.apache.org/doc/current/zookeeperAdmin.html) +**References:** [ZOOKEEPER-1045](https://issues.apache.org/jira/browse/ZOOKEEPER-1045) · [Server-Server mutual authentication](https://cwiki.apache.org/confluence/display/ZOOKEEPER/Server-Server+mutual+authentication) · [ZooKeeper Admin Guide](https://zookeeper.apache.org/doc/current/admin-ops/administrators-guide/) --- diff --git a/zookeeper-website/app/pages/_landing/irc/content.md b/zookeeper-website/app/pages/_landing/slack/content.md similarity index 60% rename from zookeeper-website/app/pages/_landing/irc/content.md rename to zookeeper-website/app/pages/_landing/slack/content.md index abc652be2f0..fd2f0bdce1b 100644 --- a/zookeeper-website/app/pages/_landing/irc/content.md +++ b/zookeeper-website/app/pages/_landing/slack/content.md @@ -16,15 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. --> -# IRC Channel +# Slack Channel -The Apache ZooKeeper community uses IRC for real-time chat and informal discussion. +The Apache ZooKeeper community uses the ASF Slack workspace for real-time chat and informal discussion. ## Channel -**Server:** `irc.libera.chat` +**Workspace:** [the-asf.slack.com](https://the-asf.slack.com/) **Channel:** `#zookeeper` -## Connecting +## Joining -The IRC channel can be used for online discussion about zookeeper related stuff, but developers should be careful to transfer all the official decisions or useful discussions to the issue tracking system. +See the [ASF Infrastructure guide to Slack](https://infra.apache.org/slack.html) for instructions on how to join the workspace. + +The Slack channel can be used for online discussion about ZooKeeper related topics, but developers should be careful to transfer all official decisions or useful discussions to the issue tracking system. diff --git a/zookeeper-website/app/pages/_landing/irc/index.tsx b/zookeeper-website/app/pages/_landing/slack/index.tsx similarity index 96% rename from zookeeper-website/app/pages/_landing/irc/index.tsx rename to zookeeper-website/app/pages/_landing/slack/index.tsx index e20bde1ad2b..ee662ec4628 100644 --- a/zookeeper-website/app/pages/_landing/irc/index.tsx +++ b/zookeeper-website/app/pages/_landing/slack/index.tsx @@ -19,6 +19,6 @@ import { MdLayout } from "@/components/mdx-components"; import Content from "./content.md"; -export function IrcPage() { +export function SlackPage() { return ; } diff --git a/zookeeper-website/app/routes.ts b/zookeeper-website/app/routes.ts index 46884abddb4..3dfb62c1e16 100644 --- a/zookeeper-website/app/routes.ts +++ b/zookeeper-website/app/routes.ts @@ -45,7 +45,7 @@ const landingRoutes = layout("./pages/_landing/landing-layout.tsx", [ route("bylaws", "routes/_landing/bylaws.tsx"), route("mailing-lists", "routes/_landing/mailing-lists.tsx"), route("security", "routes/_landing/security.tsx"), - route("irc", "routes/_landing/irc.tsx"), + route("slack", "routes/_landing/slack.tsx"), route("version-control", "routes/_landing/version-control.tsx") ]); @@ -53,8 +53,8 @@ const docRedirectRoute = route("doc", "routes/_docs/doc-redirect.tsx"); const docsBuildRoutes = [ layout("./pages/_docs/docs-layout.tsx", [ - index("routes/_docs/docs.tsx", { id: "docs-index" }), - route("*", "routes/_docs/docs.tsx", { id: "docs-splat" }) + index("routes/_docs/doc.tsx", { id: "docs-index" }), + route("*", "routes/_docs/doc.tsx", { id: "docs-splat" }) ]), route("api/search", "routes/_api/search.ts"), route("llms-full.txt", "routes/_api/llms-full.ts") @@ -69,7 +69,7 @@ const devCombinedRoutes = [ landingRoutes, docRedirectRoute, layout("./pages/_docs/docs-layout.tsx", [ - route(`doc/r${CURRENT_VERSION}/*`, "routes/_docs/docs.tsx") + route(`doc/r${CURRENT_VERSION}/*`, "routes/_docs/doc.tsx") ]), route("api/search", "routes/_api/search.ts"), route("llms-full.txt", "routes/_api/llms-full.ts") diff --git a/zookeeper-website/app/routes/_api/llms-full.ts b/zookeeper-website/app/routes/_api/llms-full.ts index e226e562fc1..94942e233d8 100644 --- a/zookeeper-website/app/routes/_api/llms-full.ts +++ b/zookeeper-website/app/routes/_api/llms-full.ts @@ -87,8 +87,5 @@ export function resolveLLMTextLinks( } function escapeRegExp(value: string): string { - // "#" is special in regex (start of comment). Escape it in the pattern only: - // in: /admin-ops/.../configuration-parameters#advanced-configuration - // out: /admin-ops/.../configuration-parameters\#advanced-configuration (RegExp string) return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/zookeeper-website/app/routes/_docs/doc-redirect.tsx b/zookeeper-website/app/routes/_docs/doc-redirect.tsx index c2cab7f9c0f..2ca1855b637 100644 --- a/zookeeper-website/app/routes/_docs/doc-redirect.tsx +++ b/zookeeper-website/app/routes/_docs/doc-redirect.tsx @@ -16,9 +16,14 @@ // limitations under the License. // -import { Navigate } from "react-router"; +import { redirect } from "react-router"; import { CURRENT_DOCS_PATH } from "@/lib/docs-paths"; +// /doc -> /doc/r/. +export function loader() { + return redirect(`${CURRENT_DOCS_PATH}/`); +} + export default function DocRedirect() { - return ; + return null; } diff --git a/zookeeper-website/app/routes/_docs/docs.tsx b/zookeeper-website/app/routes/_docs/doc.tsx similarity index 92% rename from zookeeper-website/app/routes/_docs/docs.tsx rename to zookeeper-website/app/routes/_docs/doc.tsx index 2a473845609..0ff30537aa8 100644 --- a/zookeeper-website/app/routes/_docs/docs.tsx +++ b/zookeeper-website/app/routes/_docs/doc.tsx @@ -16,7 +16,7 @@ // limitations under the License. // -import type { Route } from "./+types/docs"; +import type { Route } from "./+types/doc"; import { source } from "@/lib/source"; import { DocsPage } from "@/pages/_docs/docs"; @@ -31,6 +31,6 @@ export async function loader({ params }: Route.LoaderArgs) { }; } -export default function Docs(props: Route.ComponentProps) { +export default function Doc(props: Route.ComponentProps) { return ; } diff --git a/zookeeper-website/app/routes/_landing/irc.tsx b/zookeeper-website/app/routes/_landing/slack.tsx similarity index 76% rename from zookeeper-website/app/routes/_landing/irc.tsx rename to zookeeper-website/app/routes/_landing/slack.tsx index 4a2243c1ef8..c9130774d6e 100644 --- a/zookeeper-website/app/routes/_landing/irc.tsx +++ b/zookeeper-website/app/routes/_landing/slack.tsx @@ -16,19 +16,19 @@ // limitations under the License. // -import type { Route } from "./+types/irc"; -import { IrcPage } from "@/pages/_landing/irc"; +import type { Route } from "./+types/slack"; +import { SlackPage } from "@/pages/_landing/slack"; export function meta({}: Route.MetaArgs) { return [ - { title: "IRC Channel - Apache ZooKeeper" }, + { title: "Slack Channel - Apache ZooKeeper" }, { name: "description", - content: "Connect with the Apache ZooKeeper community on IRC." + content: "Connect with the Apache ZooKeeper community on Slack." } ]; } -export default function Irc() { - return ; +export default function Slack() { + return ; } diff --git a/zookeeper-website/e2e-tests/constants.ts b/zookeeper-website/e2e-tests/constants.ts index 549507c0d67..cebe92f9932 100644 --- a/zookeeper-website/e2e-tests/constants.ts +++ b/zookeeper-website/e2e-tests/constants.ts @@ -17,7 +17,6 @@ // import { CURRENT_VERSION } from "../app/lib/current-version"; -import { RELEASED_DOC_VERSIONS } from "../app/lib/released-docs-versions"; // Current docs are served from this base in every build. Derived from // CURRENT_VERSION so a version bump needs no e2e edits. @@ -27,7 +26,3 @@ export const DOCS_ROOT = `/doc/r${CURRENT_VERSION}/`; export const DOCS_URL_PATTERN = new RegExp( `doc/r${CURRENT_VERSION.replace(/\./g, "\\.")}` ); - -// The no-JS older-docs fallback renders every released version from the real -// list, so assert against its length instead of a hardcoded count. -export const RELEASED_DOC_VERSION_COUNT = RELEASED_DOC_VERSIONS.length; diff --git a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts index c5bd2317e9e..2321fadb28c 100644 --- a/zookeeper-website/e2e-tests/older-docs-picker.spec.ts +++ b/zookeeper-website/e2e-tests/older-docs-picker.spec.ts @@ -17,30 +17,16 @@ // import { test, expect } from "@playwright/test"; -import { DOCS_ROOT, RELEASED_DOC_VERSION_COUNT } from "./constants"; +import { DOCS_ROOT } from "./constants"; +import { + getReleasedDocUrl, + getReleasedDocVersions +} from "../app/lib/released-docs-versions"; -const MOCK_RELEASED_DOC_VERSIONS = ["3.10.0", "3.9.4", "3.9.3"]; -const RELEASED_DOC_VERSIONS_OVERRIDE_KEY = "__released_doc_versions_override__"; - -function expectedReleasedDocUrl(version: string): string { - if (version === "3.9.4" || version === "3.9.3") { - return `/doc/r${version}/index.html`; - } - - return `/doc/r${version}/`; -} +const EXPECTED_VERSIONS = getReleasedDocVersions(); test.describe("Older Docs Picker – sidebar", () => { test.beforeEach(async ({ page }) => { - await page.addInitScript( - ({ key, versions }) => { - window.localStorage.setItem(key, JSON.stringify(versions)); - }, - { - key: RELEASED_DOC_VERSIONS_OVERRIDE_KEY, - versions: MOCK_RELEASED_DOC_VERSIONS - } - ); await page.goto(DOCS_ROOT); await page.waitForLoadState("networkidle"); }); @@ -69,7 +55,7 @@ test.describe("Older Docs Picker – sidebar", () => { const options = list.getByRole("option"); await expect(options.first()).toBeVisible(); - await expect(options).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); + await expect(options).toHaveCount(EXPECTED_VERSIONS.length); }); test("versions are displayed in descending order", async ({ page }) => { @@ -79,9 +65,7 @@ test.describe("Older Docs Picker – sidebar", () => { await expect(options.first()).toBeVisible(); const texts = await options.allTextContents(); - expect(texts.map((text) => text.trim())).toEqual( - MOCK_RELEASED_DOC_VERSIONS - ); + expect(texts.map((text) => text.trim())).toEqual(EXPECTED_VERSIONS); }); test("each version item links to the correct archive path", async ({ @@ -90,10 +74,10 @@ test.describe("Older Docs Picker – sidebar", () => { await page.getByRole("button", { name: /older docs/i }).click(); const options = page.getByRole("option"); - await expect(options).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - for (let i = 0; i < MOCK_RELEASED_DOC_VERSIONS.length; i++) { + await expect(options).toHaveCount(EXPECTED_VERSIONS.length); + for (let i = 0; i < EXPECTED_VERSIONS.length; i++) { const href = await options.nth(i).getAttribute("href"); - expect(href).toBe(expectedReleasedDocUrl(MOCK_RELEASED_DOC_VERSIONS[i])); + expect(href).toBe(getReleasedDocUrl(EXPECTED_VERSIONS[i])); } }); @@ -108,20 +92,25 @@ test.describe("Older Docs Picker – sidebar", () => { const allOptions = page.getByRole("option"); const totalBefore = await allOptions.count(); - // Type a prefix that matches only a subset of versions - await input.fill("3.9"); + const [major, minor] = EXPECTED_VERSIONS[0].split("."); + const prefix = `${major}.${minor}`; + const expectedMatches = EXPECTED_VERSIONS.filter((v) => + v.startsWith(prefix) + ).length; + + await input.fill(prefix); await page.waitForTimeout(200); const filtered = page.getByRole("option"); const totalAfter = await filtered.count(); - expect(totalBefore).toBe(MOCK_RELEASED_DOC_VERSIONS.length); - expect(totalAfter).toBe(2); + expect(totalBefore).toBe(EXPECTED_VERSIONS.length); + expect(totalAfter).toBe(expectedMatches); // Every remaining option must contain the search term for (let i = 0; i < totalAfter; i++) { const text = await filtered.nth(i).textContent(); - expect(text).toContain("3.9"); + expect(text).toContain(prefix); } }); @@ -142,7 +131,7 @@ test.describe("Older Docs Picker – sidebar", () => { await trigger.click(); const input = page.getByRole("combobox"); - await input.fill("3.9"); + await input.fill("anything"); // Close the popover by pressing Escape await page.keyboard.press("Escape"); @@ -154,110 +143,3 @@ test.describe("Older Docs Picker – sidebar", () => { await expect(newInput).toHaveValue(""); }); }); - -test.describe("Older Docs Picker – navbar Documentation menu", () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript( - ({ key, versions }) => { - window.localStorage.setItem(key, JSON.stringify(versions)); - }, - { - key: RELEASED_DOC_VERSIONS_OVERRIDE_KEY, - versions: MOCK_RELEASED_DOC_VERSIONS - } - ); - await page.goto("/"); - await page.waitForLoadState("networkidle"); - }); - - test("Documentation menu contains an 'Older docs' sub-menu trigger", async ({ - page - }) => { - // Open the Documentation dropdown (scope to banner to avoid matching the mobile collapsible) - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - // The sub-menu trigger should be visible - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await expect(olderDocs).toBeVisible(); - }); - - test("hovering 'Older docs' in the navbar opens a version sub-menu", async ({ - page - }) => { - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await olderDocs.hover(); - // ArrowRight reliably opens Radix sub-menus cross-browser (hover alone is flaky in Firefox) - await olderDocs.press("ArrowRight"); - - // Wait until the sub-menu actually opens (a second menu element becomes visible) - await expect(page.getByRole("menu")).toHaveCount(2, { timeout: 10000 }); - const subMenu = page.getByRole("menu").last(); - - const versionLinks = subMenu.locator('a[href^="/doc/r"]'); - await expect(versionLinks.first()).toBeVisible(); - await expect(versionLinks).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - }); - - test("navbar older-docs links point to the correct archive paths", async ({ - page - }) => { - await page - .getByRole("banner") - .getByRole("button", { name: /documentation/i }) - .click(); - - const olderDocs = page.getByRole("menuitem", { name: /older docs/i }); - await olderDocs.hover(); - await olderDocs.press("ArrowRight"); - - const subMenu = page.getByRole("menu").last(); - await expect(subMenu).toBeVisible(); - - const links = subMenu.locator('a[href^="/doc/r"]'); - await expect(links).toHaveCount(MOCK_RELEASED_DOC_VERSIONS.length); - for (let i = 0; i < MOCK_RELEASED_DOC_VERSIONS.length; i++) { - const href = await links.nth(i).getAttribute("href"); - expect(href).toBe(expectedReleasedDocUrl(MOCK_RELEASED_DOC_VERSIONS[i])); - } - }); -}); - -test.describe("Older Docs Picker – no-JS navbar fallback", () => { - test.use({ javaScriptEnabled: false }); - - test("Documentation menu exposes older-docs links without JavaScript", async ({ - page - }) => { - await page.goto("/"); - - const documentationMenu = page - .getByRole("banner") - .locator("details") - .filter({ hasText: "Documentation" }) - .first(); - await documentationMenu.locator("summary").first().click(); - - const olderDocs = documentationMenu - .locator("details") - .filter({ hasText: "Older docs" }) - .first(); - await expect(olderDocs.locator("summary")).toBeVisible(); - - await olderDocs.locator("summary").click(); - - const links = olderDocs.locator('a[href^="/doc/r"]'); - await expect(links.first()).toBeVisible(); - await expect(links).toHaveCount(RELEASED_DOC_VERSION_COUNT); - await expect( - olderDocs.locator('a[href="/doc/r3.9.4/index.html"]') - ).toBeVisible(); - }); -}); diff --git a/zookeeper-website/package-lock.json b/zookeeper-website/package-lock.json index 3f3b90f11c1..58db82f217d 100644 --- a/zookeeper-website/package-lock.json +++ b/zookeeper-website/package-lock.json @@ -22,7 +22,7 @@ "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-separator": "1.1.1", - "@radix-ui/react-slot": "latest", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-tooltip": "1.1.6", "@react-router/node": "^7.17.0", diff --git a/zookeeper-website/package.json b/zookeeper-website/package.json index 35d46131e4d..54367988a52 100644 --- a/zookeeper-website/package.json +++ b/zookeeper-website/package.json @@ -3,11 +3,11 @@ "private": true, "type": "module", "engines": { - "node": ">=22.12.0" + "node": "^22.12.0 || ^23.0.0 || ^24.0.0" }, "scripts": { "build": "tsx scripts/build-site.ts", - "build:landing": "npm run fumadocs-init && ZOOKEEPER_BUILD_TARGET=landing react-router build && npm run generate-sitemap -- --scope landing", + "build:landing": "npm run extract-developers && ZOOKEEPER_BUILD_TARGET=landing react-router build", "build:docs": "tsx scripts/build-docs.ts", "dev": "react-router dev", "start": "vite preview --port 5173", @@ -25,9 +25,9 @@ "test:e2e:debug": "playwright test --debug", "fumadocs-init": "fumadocs-mdx", "generate-sitemap": "tsx scripts/generate-sitemap.ts", - "build:docs-archive": "tsx scripts/build-docs.ts", + "extract-developers": "node scripts/extract-developers.js", "ci": "npm run fumadocs-init && npm run lint && npm run typecheck && npm run test:unit:run && npm run build && npx playwright install && npm run test:e2e", - "ci-skip-tests": "npm run fumadocs-init && npm run build" + "ci-skip-tests": "npm run build" }, "dependencies": { "@hookform/resolvers": "^3.10.0", @@ -46,7 +46,7 @@ "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-separator": "1.1.1", - "@radix-ui/react-slot": "latest", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-tabs": "1.1.2", "@radix-ui/react-tooltip": "1.1.6", "@react-router/node": "^7.17.0", diff --git a/zookeeper-website/pom.xml b/zookeeper-website/pom.xml index b0d38ce7b85..568781dee37 100644 --- a/zookeeper-website/pom.xml +++ b/zookeeper-website/pom.xml @@ -97,7 +97,7 @@ pre-site - install + ci diff --git a/zookeeper-website/public/.htaccess b/zookeeper-website/public/.htaccess index 4b989a6561c..55aa05d65da 100644 --- a/zookeeper-website/public/.htaccess +++ b/zookeeper-website/public/.htaccess @@ -36,6 +36,8 @@ ErrorDocument 404 /404.html + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "DENY" Header always set Cache-Control "no-cache, must-revalidate" diff --git a/zookeeper-website/public/sitemap.xml b/zookeeper-website/public/sitemap.xml deleted file mode 100644 index be9b4451c35..00000000000 --- a/zookeeper-website/public/sitemap.xml +++ /dev/null @@ -1 +0,0 @@ -https://zookeeper.apache.org/https://zookeeper.apache.org/bylaws/https://zookeeper.apache.org/credits/https://zookeeper.apache.org/doc/r3.9.5/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/administration/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/best-practices/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/commands/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/communication-using-the-netty-framework/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/configuration-parameters/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/data-file-management/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/administrators-guide/deployment/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/cli/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/dynamic-reconfiguration/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/jmx/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/monitor-and-audit-logs/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/observers-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/quorums/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/quota-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/snapshot-and-restore-guide/https://zookeeper.apache.org/doc/r3.9.5/admin-ops/tools/https://zookeeper.apache.org/doc/r3.9.5/developer/basic-tutorial/https://zookeeper.apache.org/doc/r3.9.5/developer/java-example/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/access-control-using-acls/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/bindings/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/building-blocks-a-guide-to-zookeeper-operations/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/consistency-guarantees/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/data-model/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/gotchas-common-problems-and-troubleshooting/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/pluggable-authentication/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/sessions/https://zookeeper.apache.org/doc/r3.9.5/developer/programmers-guide/watches/https://zookeeper.apache.org/doc/r3.9.5/developer/recipes/https://zookeeper.apache.org/doc/r3.9.5/developer/use-cases/https://zookeeper.apache.org/doc/r3.9.5/miscellaneous/internals/https://zookeeper.apache.org/doc/r3.9.5/overview/quick-start/https://zookeeper.apache.org/doc/r3.9.5/overview/release-notes/https://zookeeper.apache.org/events/https://zookeeper.apache.org/irc/https://zookeeper.apache.org/mailing-lists/https://zookeeper.apache.org/news/https://zookeeper.apache.org/releases/https://zookeeper.apache.org/security/https://zookeeper.apache.org/version-control/ \ No newline at end of file diff --git a/zookeeper-website/scripts/build-docs.ts b/zookeeper-website/scripts/build-docs.ts index b2b4d80330f..3392c2da501 100644 --- a/zookeeper-website/scripts/build-docs.ts +++ b/zookeeper-website/scripts/build-docs.ts @@ -31,7 +31,6 @@ import { fileURLToPath } from "node:url"; import { CURRENT_VERSION } from "../app/lib/current-version"; import { DOCS_ARCHIVE_BASE_ENV, - DOCS_ARCHIVE_SNAPSHOT_ENV, normalizeDocsArchiveBase } from "../app/lib/docs-archive"; import { formatDocsBase } from "../app/lib/docs-paths"; @@ -66,19 +65,10 @@ export const ROOT_URL_PATTERNS = [ const DOCS_LOCAL_PATHS = "(?:apidocs|assets|docs-images|fonts|images)\\b[^\"')]*|favicon\\.ico"; -interface DocsBuildArgs { - snapshot: boolean; -} - -export function parseDocsBuildArgs( - args = process.argv.slice(2), - scriptName = process.env.npm_lifecycle_event -): DocsBuildArgs { +export function parseDocsBuildArgs(args = process.argv.slice(2)): void { if (args.length > 0) { - throw new Error("Usage: npm run build:docs or npm run build:docs-archive"); + throw new Error("Usage: npm run build:docs"); } - - return { snapshot: scriptName === "build:docs-archive" }; } function runCommand(command: string, args: string[], env: NodeJS.ProcessEnv) { @@ -97,13 +87,6 @@ function runCommand(command: string, args: string[], env: NodeJS.ProcessEnv) { } } -function getWebsiteCheckEnv(): NodeJS.ProcessEnv { - const env = { ...process.env }; - delete env[DOCS_ARCHIVE_BASE_ENV]; - delete env[DOCS_ARCHIVE_SNAPSHOT_ENV]; - return env; -} - async function pathExists(path: string): Promise { try { await access(path); @@ -282,21 +265,15 @@ export async function verifyArchiveOutput( // Pure, self-contained docs build for a single version. Produces a versioned // output at build/doc/r/ with its own assets, .htaccess, rewritten -// docs-local URLs, and llms-full.txt. Pass `snapshot: true` for archived -// versions so the "switch to latest" banner shows. -export async function buildDocs( - version: string, - { snapshot = false }: { snapshot?: boolean } = {} -): Promise { +// docs-local URLs, and llms-full.txt. +export async function buildDocs(version: string): Promise { const docsBase = normalizeDocsArchiveBase(formatDocsBase(version)); const env = { ...process.env, - [DOCS_ARCHIVE_BASE_ENV]: docsBase, - ...(snapshot ? { [DOCS_ARCHIVE_SNAPSHOT_ENV]: "1" } : {}) + [DOCS_ARCHIVE_BASE_ENV]: docsBase }; console.log(`Building docs for ${docsBase}`); - runCommand("npm", ["run", "fumadocs-init"], env); runCommand("npx", ["react-router", "build"], env); const outputDir = await copyDocsOutput(version); @@ -308,36 +285,14 @@ export async function buildDocs( } export async function main() { - const { snapshot } = parseDocsBuildArgs(); + parseDocsBuildArgs(); - if (snapshot) { - console.log("Running website checks before building the docs archive"); - runCommand("npm", ["run", "ci"], getWebsiteCheckEnv()); - } - - const version = CURRENT_VERSION; - await buildDocs(version, { snapshot }); - - // The current docs build owns the docs slice of the sitemap. Archive snapshots - // are intentionally excluded, so they never touch it. - if (!snapshot) { - runCommand( - "npm", - ["run", "generate-sitemap", "--", "--scope", "docs"], - process.env - ); - } + await buildDocs(CURRENT_VERSION); - if (snapshot) { - console.log(""); - console.log("Publish with:"); - console.log(` git checkout asf-site`); - console.log(` rm -rf content/doc/r${version}`); - console.log(` mkdir -p content/doc/r${version}`); - console.log( - ` cp -R zookeeper-website/build/doc/r${version}/. content/doc/r${version}/` - ); - } + // react-router build emits to build/client/, then buildDocs() assembles the + // self-contained deliverable at build/doc/r/. The leftover build/client/ + // is just intermediate output — drop it so build/doc/ is the only root. + await rm(BUILD_CLIENT_DIR, { recursive: true, force: true }); } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/zookeeper-website/scripts/build-site.ts b/zookeeper-website/scripts/build-site.ts index 28b6b2b075c..f046c3cb322 100644 --- a/zookeeper-website/scripts/build-site.ts +++ b/zookeeper-website/scripts/build-site.ts @@ -60,6 +60,9 @@ export async function main() { await rm(stashDir, { recursive: true, force: true }); await cp(docsOutputDir, stashDir, { recursive: true }); + console.log("Extracting developers from parent pom.xml"); + runCommand("npm", ["run", "extract-developers"], process.env); + console.log("Building landing site"); runCommand("npx", ["react-router", "build"], { ...process.env, diff --git a/zookeeper-website/scripts/extract-developers.d.ts b/zookeeper-website/scripts/extract-developers.d.ts new file mode 100644 index 00000000000..bd4d7e47598 --- /dev/null +++ b/zookeeper-website/scripts/extract-developers.d.ts @@ -0,0 +1,28 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export interface Developer { + id: string; + name: string; + email: string; + timezone: string; +} + +export function extractField(block: string, fieldName: string): string; +export function parseDevelopers(pomContent: string): Developer[]; +export function main(): void; diff --git a/zookeeper-website/scripts/extract-developers.js b/zookeeper-website/scripts/extract-developers.js new file mode 100644 index 00000000000..c3a6835577a --- /dev/null +++ b/zookeeper-website/scripts/extract-developers.js @@ -0,0 +1,113 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Extracts a field value from a developer XML block + * @param {string} block - The XML block containing developer information + * @param {string} fieldName - The name of the field to extract + * @returns {string} The field value or '-' if not present + */ +export function extractField(block, fieldName) { + const fieldRegex = new RegExp(`<${fieldName}>(.*?)<\/${fieldName}>`, 's'); + const fieldMatch = block.match(fieldRegex); + return fieldMatch ? fieldMatch[1].trim() : '-'; +} + +/** + * Parses developers from POM XML content + * @param {string} pomContent - The content of the pom.xml file + * @returns {Array} Array of developer objects + * @throws {Error} If no developers section is found + */ +export function parseDevelopers(pomContent) { + // Extract developers using regex + const developersMatch = pomContent.match(/([\s\S]*?)<\/developers>/); + if (!developersMatch) { + throw new Error('No developers section found in pom.xml'); + } + + const developersXml = developersMatch[1]; + // Match each developer block + const developerBlockRegex = /([\s\S]*?)<\/developer>/gs; + + const developers = []; + let match; + + while ((match = developerBlockRegex.exec(developersXml)) !== null) { + const block = match[1]; + + const id = extractField(block, 'id'); + const name = extractField(block, 'name'); + const email = extractField(block, 'email'); + const timezone = extractField(block, 'timezone'); + + developers.push({ + id, + name, + email, + timezone, + }); + } + + return developers; +} + +/** + * Main function to extract developers and write to JSON file + */ +export function main() { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + // Read the parent pom.xml + const pomPath = path.join(__dirname, '..', '..', 'pom.xml'); + const pomContent = fs.readFileSync(pomPath, 'utf-8'); + + let developers; + try { + developers = parseDevelopers(pomContent); + } catch (error) { + console.error(error.message); + process.exit(1); + } + + console.log(`Extracted ${developers.length} developers from pom.xml`); + + // Write to JSON file + const outputPath = path.join( + __dirname, + '..', + 'app', + 'pages', + '_landing', + 'credits', + 'developers.json', + ); + fs.writeFileSync(outputPath, JSON.stringify(developers, null, 2)); + + console.log(`Developers data written to ${outputPath}`); +} + +// Run main if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/zookeeper-website/scripts/generate-sitemap.ts b/zookeeper-website/scripts/generate-sitemap.ts index e67efc45fb8..fc02747e150 100644 --- a/zookeeper-website/scripts/generate-sitemap.ts +++ b/zookeeper-website/scripts/generate-sitemap.ts @@ -45,9 +45,9 @@ const EXCLUDED_HTML_PATHS = new Set([ "404.html", "404/index.html", "__spa-fallback.html", - // The /doc redirect is a client-side with no redirect marker in its - // prerendered HTML, so it must be excluded explicitly (and it only exists in - // builds that include landing, which would otherwise diverge from docs builds). + // /doc is a loader-driven redirect to /doc/r/. Its meta- + // refresh marker would already be caught by REDIRECT_PAGE_PATTERNS, but we + // exclude it explicitly so landing-inclusive and docs-only builds agree. "doc/index.html" ]); diff --git a/zookeeper-website/unit-tests/archive-docs-banner.test.tsx b/zookeeper-website/unit-tests/archive-docs-banner.test.tsx deleted file mode 100644 index fb8d3c01e4e..00000000000 --- a/zookeeper-website/unit-tests/archive-docs-banner.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { ArchiveDocsBanner } from "@/components/docs/archive-docs-banner"; -import { CURRENT_VERSION } from "@/lib/current-version"; -import { DOCS_ARCHIVE_SNAPSHOT_ENV } from "@/lib/docs-archive"; - -const storageKey = `zookeeper-docs-archive-banner-dismissed:${CURRENT_VERSION}`; - -function enableArchiveBuild() { - vi.stubEnv(DOCS_ARCHIVE_SNAPSHOT_ENV, "1"); -} - -beforeEach(() => { - localStorage.clear(); -}); - -afterEach(() => { - vi.unstubAllEnvs(); - localStorage.clear(); -}); - -describe("ArchiveDocsBanner", () => { - it("renders nothing outside an archive build", () => { - render(); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - }); - - it("shows the switch-to-latest notice on an archive snapshot", () => { - enableArchiveBuild(); - render(); - - expect( - screen.getByText(new RegExp(`viewing the v${CURRENT_VERSION} docs`, "i")) - ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: /switch to latest/i }) - ).toHaveAttribute("href", "/doc/"); - }); - - it("dismisses the banner and remembers the dismissal", async () => { - const user = userEvent.setup(); - enableArchiveBuild(); - render(); - - expect(screen.getByRole("status")).toBeInTheDocument(); - - await user.click( - screen.getByRole("button", { name: /dismiss archived docs notice/i }) - ); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - expect(localStorage.getItem(storageKey)).toBe("1"); - }); - - it("stays hidden when already dismissed", () => { - enableArchiveBuild(); - localStorage.setItem(storageKey, "1"); - render(); - - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - }); -}); diff --git a/zookeeper-website/unit-tests/build-docs-archive.test.ts b/zookeeper-website/unit-tests/build-docs.test.ts similarity index 96% rename from zookeeper-website/unit-tests/build-docs-archive.test.ts rename to zookeeper-website/unit-tests/build-docs.test.ts index aa4fb9066fe..cb2341af2a8 100644 --- a/zookeeper-website/unit-tests/build-docs-archive.test.ts +++ b/zookeeper-website/unit-tests/build-docs.test.ts @@ -74,14 +74,8 @@ afterEach(async () => { }); describe("parseDocsBuildArgs", () => { - it("uses the current version docs build by default", () => { - expect(parseDocsBuildArgs([])).toEqual({ snapshot: false }); - }); - - it("marks archive builds from the npm script name", () => { - expect(parseDocsBuildArgs([], "build:docs-archive")).toEqual({ - snapshot: true - }); + it("accepts no arguments", () => { + expect(() => parseDocsBuildArgs([])).not.toThrow(); }); it("rejects CLI version overrides", () => { diff --git a/zookeeper-website/unit-tests/extract-developers.test.ts b/zookeeper-website/unit-tests/extract-developers.test.ts new file mode 100644 index 00000000000..ea55dbfe375 --- /dev/null +++ b/zookeeper-website/unit-tests/extract-developers.test.ts @@ -0,0 +1,255 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { describe, it, expect } from "vitest"; +import { + extractField, + parseDevelopers +} from "../scripts/extract-developers.js"; + +describe("extract-developers script", () => { + const createPomXml = ( + developersXml: string + ) => ` + + +${developersXml} + +`; + + describe("extractField", () => { + it("should extract existing field value", () => { + const block = "johndoeJohn Doe"; + expect(extractField(block, "id")).toBe("johndoe"); + expect(extractField(block, "name")).toBe("John Doe"); + }); + + it('should return "-" for missing field', () => { + const block = "johndoejohn@example.com"; + expect(extractField(block, "name")).toBe("-"); + expect(extractField(block, "timezone")).toBe("-"); + }); + + it("should trim whitespace from field values", () => { + const block = " johndoe John Doe "; + expect(extractField(block, "id")).toBe("johndoe"); + expect(extractField(block, "name")).toBe("John Doe"); + }); + + it("should handle multiline field values", () => { + const block = "John\nDoe"; + const result = extractField(block, "name"); + expect(result).toContain("John"); + expect(result).toContain("Doe"); + }); + }); + + describe("parseDevelopers", () => { + it("should extract developers with all fields present", () => { + const pomXml = createPomXml(` + johndoe + John Doe + johndoe@apache.org + -8 + + + janedoe + Jane Doe + janedoe@apache.org + +1 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(2); + expect(developers[0]).toEqual({ + id: "johndoe", + name: "John Doe", + email: "johndoe@apache.org", + timezone: "-8" + }); + expect(developers[1]).toEqual({ + id: "janedoe", + name: "Jane Doe", + email: "janedoe@apache.org", + timezone: "+1" + }); + }); + + it("should handle missing name field", () => { + const pomXml = createPomXml(` + tianjy + tianjy@apache.org + +8 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "tianjy", + name: "-", + email: "tianjy@apache.org", + timezone: "+8" + }); + }); + + it("should handle missing email field", () => { + const pomXml = createPomXml(` + testuser + Test User + +0 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "testuser", + name: "Test User", + email: "-", + timezone: "+0" + }); + }); + + it("should handle multiple missing fields", () => { + const pomXml = createPomXml(` + minimaluser + minimal@apache.org + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "minimaluser", + name: "-", + email: "minimal@apache.org", + timezone: "-" + }); + }); + + it("should handle mixed complete and incomplete developer entries", () => { + const pomXml = createPomXml(` + complete + Complete User + complete@apache.org + +1 + + + incomplete + incomplete@apache.org + +2 + + + another + Another User + another@apache.org + -5 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(3); + expect(developers[0]).toEqual({ + id: "complete", + name: "Complete User", + email: "complete@apache.org", + timezone: "+1" + }); + expect(developers[1]).toEqual({ + id: "incomplete", + name: "-", + email: "incomplete@apache.org", + timezone: "+2" + }); + expect(developers[2]).toEqual({ + id: "another", + name: "Another User", + email: "another@apache.org", + timezone: "-5" + }); + }); + + it("should trim whitespace from field values", () => { + const pomXml = createPomXml(` + spacey + Spacey User + spacey@apache.org + +3 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "spacey", + name: "Spacey User", + email: "spacey@apache.org", + timezone: "+3" + }); + }); + + it("should handle fields in different order", () => { + const pomXml = createPomXml(` + +5 + Reordered User + reordered + reordered@apache.org + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0]).toEqual({ + id: "reordered", + name: "Reordered User", + email: "reordered@apache.org", + timezone: "+5" + }); + }); + + it("should handle empty developers section", () => { + const pomXml = createPomXml(""); + + const developers = parseDevelopers(pomXml); + expect(developers).toHaveLength(0); + }); + + it("should throw when developers section is missing", () => { + expect(() => parseDevelopers("")).toThrow( + /No developers section found/ + ); + }); + + it("should handle special characters in field values", () => { + const pomXml = createPomXml(` + special&user + User <Name> + user@example.org + +0 + `); + + const developers = parseDevelopers(pomXml); + + expect(developers).toHaveLength(1); + expect(developers[0].id).toBe("special&user"); + expect(developers[0].name).toBe("User <Name>"); + }); + }); +}); diff --git a/zookeeper-website/unit-tests/llms-full.test.ts b/zookeeper-website/unit-tests/llms-full.test.ts index e4684dcdd00..68cdc6a4a6c 100644 --- a/zookeeper-website/unit-tests/llms-full.test.ts +++ b/zookeeper-website/unit-tests/llms-full.test.ts @@ -86,6 +86,16 @@ describe("resolveLLMTextLinks", () => { ).toBe(text); }); + it("rewrites links with hash fragments in the path", () => { + const href = + "/admin-ops/administrators-guide/configuration-parameters#advanced-configuration"; + const text = `[Advanced Configuration](${href})`; + + expect(resolveLLMTextLinks(text, [{ href }])).toBe( + `[Advanced Configuration](${CURRENT_DOCS_PATH}${href})` + ); + }); + it("rewrites links inside MDX JSX text blocks when Fumadocs extracted them", () => { const text = ` See [Dynamic Reconfiguration](/admin-ops/dynamic-reconfiguration). diff --git a/zookeeper-website/unit-tests/older-docs-picker.test.tsx b/zookeeper-website/unit-tests/older-docs-picker.test.tsx index 1baad0e13b2..5738c061286 100644 --- a/zookeeper-website/unit-tests/older-docs-picker.test.tsx +++ b/zookeeper-website/unit-tests/older-docs-picker.test.tsx @@ -23,9 +23,8 @@ import { renderWithProviders } from "./utils"; import { OlderDocsPicker } from "@/components/docs/older-docs-picker"; import { getReleasedDocUrl, - LEGACY_RELEASED_DOC_VERSIONS, + RAW_RELEASED_DOC_VERSIONS, RELEASED_DOC_VERSIONS, - REACT_ROUTER_RELEASED_DOC_VERSIONS, sortVersionsDesc } from "@/lib/released-docs-versions"; @@ -73,22 +72,16 @@ describe("RELEASED_DOC_VERSIONS", () => { expect(RELEASED_DOC_VERSIONS).toEqual(sorted); }); - it("combines legacy and React Router archive versions", () => { + it("matches the sorted raw archive versions", () => { expect(RELEASED_DOC_VERSIONS).toEqual( - sortVersionsDesc([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]) + sortVersionsDesc([...RAW_RELEASED_DOC_VERSIONS]) ); }); }); describe("getReleasedDocUrl", () => { - it("uses /index.html for legacy static archives", () => { - expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/index.html"); - }); - - it("uses /doc/r/ for React Router archives", () => { + it("links every archived version to /doc/r/", () => { + expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/"); expect(getReleasedDocUrl("3.10.0")).toBe("/doc/r3.10.0/"); }); }); diff --git a/zookeeper-website/unit-tests/released-docs-versions.test.ts b/zookeeper-website/unit-tests/released-docs-versions.test.ts index 4b10e123b45..0d6c858e703 100644 --- a/zookeeper-website/unit-tests/released-docs-versions.test.ts +++ b/zookeeper-website/unit-tests/released-docs-versions.test.ts @@ -19,9 +19,7 @@ import { describe, it, expect } from "vitest"; import { getReleasedDocUrl, - LEGACY_RELEASED_DOC_VERSIONS, RAW_RELEASED_DOC_VERSIONS, - REACT_ROUTER_RELEASED_DOC_VERSIONS, RELEASED_DOC_VERSIONS, sortVersionsDesc } from "@/lib/released-docs-versions"; @@ -51,8 +49,8 @@ describe("sortVersionsDesc with mocked released-docs versions", () => { }); describe("RAW_RELEASED_DOC_VERSIONS", () => { - it("exposes an array", () => { - expect(Array.isArray(RAW_RELEASED_DOC_VERSIONS)).toBe(true); + it("exposes a Set", () => { + expect(RAW_RELEASED_DOC_VERSIONS).toBeInstanceOf(Set); }); it("strips the leading 'r' prefix from every folder name", () => { @@ -74,52 +72,28 @@ describe("RAW_RELEASED_DOC_VERSIONS", () => { }); it("contains the first and latest archived documentation versions", () => { - expect(RAW_RELEASED_DOC_VERSIONS).toContain("3.1.2"); - expect(RAW_RELEASED_DOC_VERSIONS).toContain("3.9.4"); - }); - - it("contains every archived docs version exactly once", () => { - expect(new Set(RAW_RELEASED_DOC_VERSIONS).size).toBe( - RAW_RELEASED_DOC_VERSIONS.length - ); - expect(RAW_RELEASED_DOC_VERSIONS.length).toBe(52); - }); - - it("combines legacy and React Router archive versions", () => { - expect(RAW_RELEASED_DOC_VERSIONS).toEqual([ - ...LEGACY_RELEASED_DOC_VERSIONS, - ...REACT_ROUTER_RELEASED_DOC_VERSIONS - ]); - }); - - it("keeps legacy and React Router archive version sets distinct", () => { - const legacyVersions = new Set(LEGACY_RELEASED_DOC_VERSIONS); - REACT_ROUTER_RELEASED_DOC_VERSIONS.forEach((version) => { - expect(legacyVersions.has(version)).toBe(false); - }); + expect(RAW_RELEASED_DOC_VERSIONS.has("3.1.2")).toBe(true); + expect(RAW_RELEASED_DOC_VERSIONS.has("3.9.4")).toBe(true); }); }); describe("RELEASED_DOC_VERSIONS (sorted output)", () => { it("equals sortVersionsDesc applied to the raw virtual-module data", () => { expect(RELEASED_DOC_VERSIONS).toEqual( - sortVersionsDesc(RAW_RELEASED_DOC_VERSIONS) + sortVersionsDesc([...RAW_RELEASED_DOC_VERSIONS]) ); }); it("contains the same entries as RAW_RELEASED_DOC_VERSIONS, just reordered", () => { expect(RELEASED_DOC_VERSIONS.slice().sort()).toEqual( - RAW_RELEASED_DOC_VERSIONS.slice().sort() + [...RAW_RELEASED_DOC_VERSIONS].sort() ); }); }); describe("getReleasedDocUrl", () => { - it("links legacy static archives to index.html", () => { - expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/index.html"); - }); - - it("links new React Router archives to /doc/r/", () => { + it("links every archived version to /doc/r/", () => { + expect(getReleasedDocUrl("3.9.4")).toBe("/doc/r3.9.4/"); expect(getReleasedDocUrl("3.10.0")).toBe("/doc/r3.10.0/"); }); });
  7. Username NameOrganizationEmail Time Zone
    {member.id}{member.name || "-"} - {member.organization || "-"} - {member.timezone || "-"}{developer.id}{developer.name}{developer.email}{developer.timezone}