From 30aa1f12af77d5f362d38ac5487d870aa9a6d29e Mon Sep 17 00:00:00 2001 From: bcastets-robotiq Date: Thu, 24 Sep 2026 16:33:49 -0400 Subject: [PATCH 1/5] Add per-tool documentation versioning (Latest/Stable/Previous versions) Each Robotiq-maintained, submodule-synced tool (Tactile Sensor C++/Python, Adaptive grippers C++, Isaac Sim) now gets its own independent Latest/Stable/Previous-versions switcher, backed by its own Docusaurus plugin-content-docs instance with content under versioned-tools/ instead of docs/. A shared nav-tree module (scripts/site-nav-tree.mjs) keeps the full site sidebar visible on every versioned page instead of swapping in an isolated one, using plain encodeURI()-escaped links rather than Docusaurus's pathname:// scheme (which silently opens links in a new tab). The version dropdown is scoped to its own tool via a swizzled navbar item, and the default version banner is shortened to one line. Also fixes CI's "generated content is committed" check to cover versioned-tools/ (generate-tools-table.js rewrites wrapper pages there too) and adds the matching versioned-tools/**/*.md gitignore rule so synced content isn't accidentally tracked. docs/contribute/ is updated throughout to document the new architecture, including a new versioning.mdx walkthrough and a fix for a pre-existing stale reference to a deleted script. Co-Authored-By: Claude Sonnet 5 --- .github/dependabot.yml | 11 + .github/workflows/ci.yml | 18 +- .gitignore | 8 + .../version-previous-versions/_readme.md | 14 + .../version-previous-versions/index.mdx | 10 + .../version-stable/_readme.md | 191 +++++++++++ .../version-stable/index.mdx | 21 ++ .../version-previous-versions-sidebars.json | 208 ++++++++++++ .../version-stable-sidebars.json | 208 ++++++++++++ adaptive-grippers-cpp_versions.json | 4 + docs/contribute/adding-a-tool.mdx | 19 +- docs/contribute/api-reference-cpp.mdx | 75 +++-- docs/contribute/how-it-works.mdx | 113 +++++-- docs/contribute/index.mdx | 11 +- docs/contribute/quick-reference.mdx | 16 +- docs/contribute/tools-tables.mdx | 7 +- docs/contribute/versioning.mdx | 272 ++++++++++++++++ docusaurus.config.js | 132 ++++++++ draft/documentation-versioning-summary.md | 57 ++++ draft/documentation-versioning-summary.pdf | Bin 0 -> 4956 bytes draft/documentation-versioning.md | 268 +++++++++++++++ scripts/external-jobs.js | 41 ++- scripts/folder-sidebar.mjs | 14 +- scripts/generate-tools-table.js | 23 +- scripts/list-submodule-tags.js | 81 +++++ scripts/site-nav-tree.mjs | 203 ++++++++++++ scripts/sync-external-docs.js | 110 +++++-- sidebars.adaptive-grippers-cpp.js | 73 +++++ sidebars.isaac-sim.js | 19 ++ sidebars.js | 214 ++---------- sidebars.tactile-cpp.js | 19 ++ sidebars.tactile-python.js | 19 ++ src/theme/DocVersionBanner/index.jsx | 49 +++ src/theme/NavbarItem/ComponentTypes.js | 10 + .../NavbarItem/ScopedDocsVersionDropdown.jsx | 38 +++ .../version-previous-versions/_readme.md | 304 ++++++++++++++++++ .../version-previous-versions/index.mdx | 12 + .../version-stable/_readme.md | 304 ++++++++++++++++++ .../version-stable}/index.mdx | 0 .../version-previous-versions-sidebars.json | 208 ++++++++++++ .../version-stable-sidebars.json | 208 ++++++++++++ tactile-cpp_versions.json | 1 + .../version-previous-versions/_readme.md | 170 ++++++++++ .../version-previous-versions/index.mdx | 12 + .../version-stable/_readme.md | 170 ++++++++++ .../version-stable}/index.mdx | 0 .../version-previous-versions-sidebars.json | 208 ++++++++++++ .../version-stable-sidebars.json | 208 ++++++++++++ tactile-python_versions.json | 1 + .../Libraries/C++/docs/index.mdx | 0 .../Adaptive grippers/Libraries/C++/index.mdx | 0 .../Simulation/Isaac Sim/index.mdx | 0 .../Tactile Sensor/Libraries/C++/index.mdx | 25 ++ .../Tactile Sensor/Libraries/Python/index.mdx | 25 ++ 54 files changed, 4131 insertions(+), 301 deletions(-) create mode 100644 adaptive-grippers-cpp_versioned_docs/version-previous-versions/_readme.md create mode 100644 adaptive-grippers-cpp_versioned_docs/version-previous-versions/index.mdx create mode 100644 adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md create mode 100644 adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx create mode 100644 adaptive-grippers-cpp_versioned_sidebars/version-previous-versions-sidebars.json create mode 100644 adaptive-grippers-cpp_versioned_sidebars/version-stable-sidebars.json create mode 100644 adaptive-grippers-cpp_versions.json create mode 100644 docs/contribute/versioning.mdx create mode 100644 draft/documentation-versioning-summary.md create mode 100644 draft/documentation-versioning-summary.pdf create mode 100644 draft/documentation-versioning.md create mode 100644 scripts/list-submodule-tags.js create mode 100644 scripts/site-nav-tree.mjs create mode 100644 sidebars.adaptive-grippers-cpp.js create mode 100644 sidebars.isaac-sim.js create mode 100644 sidebars.tactile-cpp.js create mode 100644 sidebars.tactile-python.js create mode 100644 src/theme/DocVersionBanner/index.jsx create mode 100644 src/theme/NavbarItem/ComponentTypes.js create mode 100644 src/theme/NavbarItem/ScopedDocsVersionDropdown.jsx create mode 100644 tactile-cpp_versioned_docs/version-previous-versions/_readme.md create mode 100644 tactile-cpp_versioned_docs/version-previous-versions/index.mdx create mode 100644 tactile-cpp_versioned_docs/version-stable/_readme.md rename {docs/drivers/Tactile Sensor/Libraries/C++ => tactile-cpp_versioned_docs/version-stable}/index.mdx (100%) create mode 100644 tactile-cpp_versioned_sidebars/version-previous-versions-sidebars.json create mode 100644 tactile-cpp_versioned_sidebars/version-stable-sidebars.json create mode 100644 tactile-cpp_versions.json create mode 100644 tactile-python_versioned_docs/version-previous-versions/_readme.md create mode 100644 tactile-python_versioned_docs/version-previous-versions/index.mdx create mode 100644 tactile-python_versioned_docs/version-stable/_readme.md rename {docs/drivers/Tactile Sensor/Libraries/Python => tactile-python_versioned_docs/version-stable}/index.mdx (100%) create mode 100644 tactile-python_versioned_sidebars/version-previous-versions-sidebars.json create mode 100644 tactile-python_versioned_sidebars/version-stable-sidebars.json create mode 100644 tactile-python_versions.json rename {docs/drivers => versioned-tools}/Adaptive grippers/Libraries/C++/docs/index.mdx (100%) rename {docs/drivers => versioned-tools}/Adaptive grippers/Libraries/C++/index.mdx (100%) rename {docs/drivers => versioned-tools}/Adaptive grippers/Simulation/Isaac Sim/index.mdx (100%) create mode 100644 versioned-tools/Tactile Sensor/Libraries/C++/index.mdx create mode 100644 versioned-tools/Tactile Sensor/Libraries/Python/index.mdx diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 79dcc78..74729bd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,3 +13,14 @@ updates: directory: "/" schedule: interval: "daily" + # Without this, Dependabot applies a default 3-day cooldown to version + # updates and ignores any tool-repo commit younger than that, so a docs + # change took 3-4 days to reach the site and "Check for updates" didn't + # help. The cooldown guards against freshly published third-party + # releases; every submodule here is a first-party Robotiq repo, so it's + # switched off for all of them. `default-days` can't be 0 (minimum 1), + # hence excluding every dependency instead. + cooldown: + default-days: 1 + exclude: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf3048e..9fa06ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,14 +65,18 @@ jobs: # tool/guide page that's grown a self-installed sub-section list — all # committed files, none of them gitignored (only the submodule-synced # docs/drivers/**/*.md content is — see the comment above that rule in - # .gitignore). Nothing else regenerates and commits these after a - # merge, so a PR that changes something the generator depends on - # (e.g. a Dependabot submodule bump) without also re-running it - # locally would otherwise silently ship a deployed site that doesn't - # match what's committed. This catches that here, generically — no - # file list to keep in sync as the generator's own output grows. + # .gitignore). It does the exact same thing under versioned-tools/ for + # a per-tool-versioned tool's own wrapper page (see + # docs/contribute/versioning.mdx) — same gitignore split there + # (versioned-tools/**/*.md), so this check covers both directories. + # Nothing else regenerates and commits these after a merge, so a PR + # that changes something the generator depends on (e.g. a Dependabot + # submodule bump) without also re-running it locally would otherwise + # silently ship a deployed site that doesn't match what's committed. + # This catches that here, generically — no file list to keep in sync + # as the generator's own output grows. - name: Verify generated content is committed - run: git diff --exit-code -- docs/ + run: git diff --exit-code -- docs/ versioned-tools/ # Smoke-tests the generated output (key pages + tools table present). - name: Test diff --git a/.gitignore b/.gitignore index cfcd3cb..46d89ac 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,14 @@ docs/drivers/**/*.md # job's actual output before adding the blanket rule above. !docs/drivers/Force Torque Sensor/Libraries/C/_readme.md +# Same rule, same reasoning, for a per-tool-versioned tool's live content — +# see docs/contribute/versioning.mdx. This is the *live*, always-regenerated +# 'Latest' content; it is NOT the same thing as a cut version's own frozen +# snapshot (_versioned_docs/, _versioned_sidebars/, +# _versions.json), which IS committed — a version cut is a deliberate, +# permanent record, not build output. +versioned-tools/**/*.md + # doxygen2docusaurus scratch output (scripts/sync-external-docs.js stages # here, then copies the filtered result into docs/ — see external-jobs.js) /.doxygen2docusaurus-staging diff --git a/adaptive-grippers-cpp_versioned_docs/version-previous-versions/_readme.md b/adaptive-grippers-cpp_versioned_docs/version-previous-versions/_readme.md new file mode 100644 index 0000000..a9c3674 --- /dev/null +++ b/adaptive-grippers-cpp_versioned_docs/version-previous-versions/_readme.md @@ -0,0 +1,14 @@ + + +C++ driver with functions to control `Robotiq` Adaptive Grippers: 2F85, 2F140 and Hand-E. It allows high communication frequency. + +:::note +With the default baudrate of the gripper the maximum achievable communication +frequency is 250Hz. The communication frequency is set with the +`ConnectionConfig::connectionFrequency` parameter, which defaults to 100Hz. +::: + +Cross-platform: Linux, Windows, macOS — and freestanding/RTOS targets such +as STM32 microcontrollers. + + diff --git a/adaptive-grippers-cpp_versioned_docs/version-previous-versions/index.mdx b/adaptive-grippers-cpp_versioned_docs/version-previous-versions/index.mdx new file mode 100644 index 0000000..eff45df --- /dev/null +++ b/adaptive-grippers-cpp_versioned_docs/version-previous-versions/index.mdx @@ -0,0 +1,10 @@ +--- +title: C++ +sidebar_label: C++ +--- + +**Stable** currently tracks `2f85_cpp`'s only tagged release so far, +**v1.0.0** — there are no older releases archived here yet. + +Once a newer tag is cut, older releases will be listed here with a link +to their own tag in the source repository. diff --git a/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md b/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md new file mode 100644 index 0000000..53bb8d4 --- /dev/null +++ b/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md @@ -0,0 +1,191 @@ +A standalone, ROS-independent C++ SDK for controlling Robotiq 2F adaptive +grippers (2F-85 / 2F-140 / Hand-E class) over their Modbus RTU serial link. +Cross-platform: Linux, Windows, macOS. + +The [ROS 2 driver](https://github.com/robotiq/ros) will consume this SDK. + +## Design + +A layered API around a shared process image: + +- **`Gripper`** — the API for applications. Construction opens the + link, reads the gripper status (it fails when no gripper answers), and + starts exchanging. All Modbus traffic happens in the background + exchange cycle (one FC 0x17 transaction per period, up to ~200 Hz at + 115200 baud). The command image is seeded from the gripper's own + state echoes before anything is written — connecting never disturbs + a running gripper. + +`setCommand()`/`getStatus()` exchange whole `GripperCommand`/`GripperStatus` +blocks. Each block has named fields (`command.positionRequest`, +`command.speed`, ...) and small accessors for its packed action/status +byte, plus the raw bytes through `data()`. Reads stay whole-snapshot, so +consecutive fields never come from different exchange cycles. The block byte layout and status +bit masks are published in `Robotiq/gripper/register_map.hpp`, and the +Modbus register addresses in `Robotiq/detail/modbus_constants.hpp`, mirroring +the instruction manual. + +The Modbus protocol layer is [nanoMODBUS](https://github.com/debevv/nanoMODBUS); +serial transport is [libserialport](https://sigrok.org/wiki/Libserialport). + +## Building + +Requirements: CMake ≥ 3.16, a C++17 compiler, libserialport. + +| Platform | libserialport | +|----------|----------------| +| Ubuntu/Debian | `sudo apt install libserialport-dev` | +| macOS | `brew install libserialport` | +| Windows | MSYS2 — see [Windows (MSYS2)](#windows-msys2) below | + +```sh +cmake -S sdk_cpp -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j +ctest --test-dir build # run unit tests, no hardware needed +``` + +### Windows (MSYS2) + +Neither vcpkg nor Conan Center packages libserialport, so the supported +Windows toolchain is MSYS2/GCC — the same environment this repo's CI +uses. MSYS2 is a Windows distribution of Unix tooling with `pacman` +(the Arch Linux package manager) and a large repository of prebuilt +native libraries. + +1. Install MSYS2 from [msys2.org](https://www.msys2.org) + (or `winget install MSYS2.MSYS2`). +2. Open the **MSYS2 UCRT64** shell from the Start menu. +3. Install the toolchain and dependencies: + + ```sh + pacman -Syu + pacman -S --needed mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-ninja \ + mingw-w64-ucrt-x86_64-libserialport + ``` + +4. Build and test as usual, from the same shell: + + ```sh + cmake -S sdk_cpp -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build -j + ctest --test-dir build + ``` + +This produces native Windows binaries (GCC, no emulation layer). MSVC +is not currently supported: libserialport ships no MSVC package, so a +Visual Studio build would have to compile libserialport itself. + +## Getting started + +A complete example — waiting for motion to settle, reading the +position back, injecting a log sink — is built as described in the Building section +and can be found here: +[`sdk_cpp/examples/move_gripper.cpp`](https://github.com/robotiq/grippers/blob/main/sdk_cpp/examples/move_gripper.cpp) + +Run it by executing: +```sh +./build/examples/move_gripper /dev/ttyUSB0 # Linux (macOS: /dev/tty.usbserial-XXXX) +./build/examples/move_gripper.exe COM3 # Windows: find the port in Device Manager +``` + +The example activates the gripper (calibration sweep), opens, and +closes — keep the jaws clear. + +### Without a gripper + +`makeFakeGripper()` returns a `Gripper` driving a fake device instead of a +serial port, for bring-up, demos and CI on machines with no hardware attached: + +```cpp +#include + +auto gripper = Robotiq::makeFakeGripper(); // no port opened +``` + +Everything above the wire is the real thing — the typed blocks, the exchange +cycle, the process image, `activate()` / `recoverFromFault()`. The device below +it is deliberately minimal: activation completes instantly and the fingers are +wherever they were last commanded to be. There is no motion profile, no travel +time, no object detection and no fault injection. + +## Consuming from CMake + +```cmake +find_package(grippers REQUIRED) # installed +# or: add_subdirectory(path/to/grippers/sdk_cpp) +target_link_libraries(your_target PRIVATE Robotiq::grippers) +``` + +## Serial port notes + +- **Linux**: add yourself to the `dialout` group for `/dev/ttyUSB*` access. + The SDK sets the FTDI `latency_timer` to 1 ms automatically when it has + permission (the kernel default of 16 ms triples Modbus latency); for + unprivileged use, ship a udev rule that sets it at plug time. +- **Windows**: the FTDI latency timer is a driver setting (Device Manager → + COM port → Port Settings → Advanced → Latency Timer); set it to 1 ms for + high-rate control. +- **macOS**: the FTDI latency timer defaults to 16 ms — capping the exchange + rate near ~60 Hz — and macOS offers no way to lower it from the SDK. To run + faster, install [FTDI's VCP driver](https://ftdichip.com/drivers/vcp-drivers/) + and set its `LatencyTimer` to `1` (in the driver's `Info.plist`); it then + applies to every open, including this SDK's. On macOS 11+ also approve the + driver in System Settings → Privacy & Security and make sure it — not + Apple's built-in FTDI driver — binds your adapter (`kextstat | grep -i ftdi`). + Otherwise ~60 Hz is the ceiling on the default driver. +- Factory-default link settings: 115200 baud, 8N1, Modbus slave 0x09. +- Port naming: `/dev/ttyUSB0` on Linux, `COM3` on Windows, + `/dev/tty.usbserial-XXXX` on macOS. +- **Windows**: thread pacing is quantized by the OS timer (default tick + ~15.6 ms), so exchange periods shorter than ~16 ms will run slower + than configured. High-rate control on Windows is currently untuned — + open an issue if your application needs it. + +## Embedded / bare-metal builds + +The SDK core — `Gripper` and its threaded exchange loop — compiles for +freestanding targets (e.g. STM32 microcontrollers, arm-none-eabi). Everything +OS-flavored is injectable; two CMake options select what ships with it: + +- **`GRIPPERS_HOSTED=OFF`** (default ON) drops the hosted conveniences: the + `std::thread`-backed `Platform` (`makeDefaultPlatform()`), and + the stderr default logger. Construct `Gripper` with its platform-taking + constructor and a `Platform` implemented over your RTOS. + `ports/threadx/threadx_platform.hpp` is the working reference (Azure RTOS + ThreadX, with the exchange task's stack size and priority as constructor + arguments); porting to another RTOS means implementing its four members over + the native primitives. +- **`GRIPPERS_BUILD_DEFAULT_SERIAL=OFF`** (default follows `GRIPPERS_HOSTED`) + drops the libserialport-backed `DefaultSerial` and its dependency. Inject + your own `Serial` (e.g. a UART transport) via the + `unique_ptr` constructors of `detail::GripperModbusClient` / + `Gripper`. +- **`detail::GripperModbusClient`** is the no-thread layer: one Modbus transaction + per call, so a single-threaded superloop schedules the exchange itself. This is + the simplest path for small MCUs and needs no RTOS — and no `Platform`. + +Two integration caveats, detailed in `ports/threadx/threadx_platform.hpp` +because each presents as an unexplained hang: the injected `Serial::read` must +yield the CPU while awaiting bytes (interrupt/DMA + RTOS semaphore, never a +polled busy-wait), and `std::chrono::steady_clock` must be backed by a real +monotonic clock on the target. + +## Versioning + +[Semantic versioning](https://semver.org) from 1.0.0 on: patch releases fix +bugs, minor releases add API, and a breaking change to the documented API takes +a major release. The documented API is what this README and the public headers +describe — `Gripper`, the command/status blocks and the register map, +`ConnectionConfig`, `Serial`, `Platform`, `Logger`, and +`detail::GripperModbusClient` for the no-thread path. Anything under +`Robotiq/detail/` that is not described here is internal and may change in any +release. + +## License + +BSD-3-Clause. Portions derived from PickNik Robotics' +[ros2_robotiq_gripper](https://github.com/PickNikRobotics/ros2_robotiq_gripper) +driver (BSD-3-Clause); original copyright notices are preserved in the +affected files and full history is preserved in git. diff --git a/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx b/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx new file mode 100644 index 0000000..c30f48a --- /dev/null +++ b/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx @@ -0,0 +1,21 @@ +--- +title: C++ +sidebar_label: C++ +--- + +
+ 2F-85 gripper +
+ +![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) + +![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) + +import Readme from './_readme.md'; + + + +## Source Code + + GitHub Repository + diff --git a/adaptive-grippers-cpp_versioned_sidebars/version-previous-versions-sidebars.json b/adaptive-grippers-cpp_versioned_sidebars/version-previous-versions-sidebars.json new file mode 100644 index 0000000..8505830 --- /dev/null +++ b/adaptive-grippers-cpp_versioned_sidebars/version-previous-versions-sidebars.json @@ -0,0 +1,208 @@ +{ + "adaptiveGrippersCppSidebar": [ + { + "type": "link", + "label": "Overview", + "href": "/docs/intro" + }, + { + "type": "category", + "label": "Adaptive grippers", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "doc", + "id": "index", + "label": "C++" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Adaptive%20grippers/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Lyrical", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Lyrical" + }, + { + "type": "link", + "label": "ROS2 · Jazzy", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Jazzy" + }, + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Humble" + }, + { + "type": "link", + "label": "ROS1 · Melodic", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Melodic" + }, + { + "type": "link", + "label": "ROS1 · Kinetic", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Kinetic" + }, + { + "type": "link", + "label": "ROS1 · Indigo", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Indigo" + } + ] + }, + { + "type": "category", + "label": "Simulation", + "items": [ + { + "type": "link", + "label": "Isaac Sim", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/Isaac%20Sim" + }, + { + "type": "link", + "label": "PyBullet", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/PyBullet" + }, + { + "type": "link", + "label": "MuJoCo", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/MuJoCo" + } + ] + }, + { + "type": "category", + "label": "Other", + "items": [ + { + "type": "link", + "label": "GraspGen", + "href": "/docs/drivers/Adaptive%20grippers/Other/GraspGen" + } + ] + } + ] + }, + { + "type": "category", + "label": "Tactile Sensor", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "link", + "label": "C++", + "href": "/docs/drivers/Tactile%20Sensor/Libraries/C++" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Tactile%20Sensor/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Lyrical", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Lyrical" + }, + { + "type": "link", + "label": "ROS2 · Jazzy", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Jazzy" + }, + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Humble" + }, + { + "type": "link", + "label": "ROS1 · Noetic", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS1-Noetic" + } + ] + }, + { + "type": "category", + "label": "Simulation", + "items": [ + { + "type": "link", + "label": "Isaac Sim", + "href": "/docs/drivers/Tactile%20Sensor/Simulation/Isaac%20Sim" + } + ] + } + ] + }, + { + "type": "category", + "label": "Force Torque Sensor", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "link", + "label": "C", + "href": "/docs/drivers/Force%20Torque%20Sensor/Libraries/C" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Force%20Torque%20Sensor/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Force%20Torque%20Sensor/ROS/ROS2-Humble" + } + ] + } + ] + }, + { + "type": "category", + "label": "EPick", + "items": [ + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/EPick/ROS/ROS2-Humble" + } + ] + } + ] + } + ] +} diff --git a/adaptive-grippers-cpp_versioned_sidebars/version-stable-sidebars.json b/adaptive-grippers-cpp_versioned_sidebars/version-stable-sidebars.json new file mode 100644 index 0000000..8505830 --- /dev/null +++ b/adaptive-grippers-cpp_versioned_sidebars/version-stable-sidebars.json @@ -0,0 +1,208 @@ +{ + "adaptiveGrippersCppSidebar": [ + { + "type": "link", + "label": "Overview", + "href": "/docs/intro" + }, + { + "type": "category", + "label": "Adaptive grippers", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "doc", + "id": "index", + "label": "C++" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Adaptive%20grippers/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Lyrical", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Lyrical" + }, + { + "type": "link", + "label": "ROS2 · Jazzy", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Jazzy" + }, + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS2-Humble" + }, + { + "type": "link", + "label": "ROS1 · Melodic", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Melodic" + }, + { + "type": "link", + "label": "ROS1 · Kinetic", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Kinetic" + }, + { + "type": "link", + "label": "ROS1 · Indigo", + "href": "/docs/drivers/Adaptive%20grippers/ROS/ROS1-Indigo" + } + ] + }, + { + "type": "category", + "label": "Simulation", + "items": [ + { + "type": "link", + "label": "Isaac Sim", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/Isaac%20Sim" + }, + { + "type": "link", + "label": "PyBullet", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/PyBullet" + }, + { + "type": "link", + "label": "MuJoCo", + "href": "/docs/drivers/Adaptive%20grippers/Simulation/MuJoCo" + } + ] + }, + { + "type": "category", + "label": "Other", + "items": [ + { + "type": "link", + "label": "GraspGen", + "href": "/docs/drivers/Adaptive%20grippers/Other/GraspGen" + } + ] + } + ] + }, + { + "type": "category", + "label": "Tactile Sensor", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "link", + "label": "C++", + "href": "/docs/drivers/Tactile%20Sensor/Libraries/C++" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Tactile%20Sensor/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Lyrical", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Lyrical" + }, + { + "type": "link", + "label": "ROS2 · Jazzy", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Jazzy" + }, + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS2-Humble" + }, + { + "type": "link", + "label": "ROS1 · Noetic", + "href": "/docs/drivers/Tactile%20Sensor/ROS/ROS1-Noetic" + } + ] + }, + { + "type": "category", + "label": "Simulation", + "items": [ + { + "type": "link", + "label": "Isaac Sim", + "href": "/docs/drivers/Tactile%20Sensor/Simulation/Isaac%20Sim" + } + ] + } + ] + }, + { + "type": "category", + "label": "Force Torque Sensor", + "items": [ + { + "type": "category", + "label": "Libraries", + "items": [ + { + "type": "link", + "label": "C", + "href": "/docs/drivers/Force%20Torque%20Sensor/Libraries/C" + }, + { + "type": "link", + "label": "Python", + "href": "/docs/drivers/Force%20Torque%20Sensor/Libraries/Python" + } + ] + }, + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/Force%20Torque%20Sensor/ROS/ROS2-Humble" + } + ] + } + ] + }, + { + "type": "category", + "label": "EPick", + "items": [ + { + "type": "category", + "label": "ROS", + "items": [ + { + "type": "link", + "label": "ROS2 · Humble", + "href": "/docs/drivers/EPick/ROS/ROS2-Humble" + } + ] + } + ] + } + ] +} diff --git a/adaptive-grippers-cpp_versions.json b/adaptive-grippers-cpp_versions.json new file mode 100644 index 0000000..402035e --- /dev/null +++ b/adaptive-grippers-cpp_versions.json @@ -0,0 +1,4 @@ +[ + "stable", + "previous-versions" +] diff --git a/docs/contribute/adding-a-tool.mdx b/docs/contribute/adding-a-tool.mdx index 7f54c8b..8a96a87 100644 --- a/docs/contribute/adding-a-tool.mdx +++ b/docs/contribute/adding-a-tool.mdx @@ -7,7 +7,18 @@ displayed_sidebar: contributeSidebar # Step-by-step: adding a new software tool The steps below add a Python tool for the **Adaptive grippers** family hosted -at `https://github.com/robotiq/2f85-python-driver`. +at `https://github.com/robotiq/2f85-python-driver`, living under `docs/` +like every non-versioned tool. + +**If the new tool is Robotiq-maintained and submodule-synced** (as opposed +to a third-party or not-yet-available tool), it should usually also get its +own **Latest / Stable / Previous versions** switcher, matching every other +Robotiq-maintained tool on this site — see +[Per-tool documentation versioning](./versioning) once the steps below are +done. That changes *where* the tool's files live (`versioned-tools/` +instead of `docs/`) and adds a few extra pieces (its own Docusaurus plugin +instance, its own `sidebars..js`); everything below — the sync job, +the wrapper page, the badges — stays exactly the same either way. ## 1. Add the submodule @@ -284,3 +295,9 @@ sub-category matching where they live on disk: Only include the sub-categories that actually have pages — e.g. Force Torque Sensor has no Simulation or Other tools yet, so its sidebar entry skips those two groups entirely. + +**Adding versioning to this tool afterward** replaces its doc-id string +above (e.g. `'drivers/Adaptive grippers/Libraries/Python/index'`) with a +plain link into its own new plugin instance — see +[Per-tool documentation versioning](./versioning#checklist-adding-versioning-to-a-new-tool) +for the full checklist. diff --git a/docs/contribute/api-reference-cpp.mdx b/docs/contribute/api-reference-cpp.mdx index 6c0396f..6ee2a3a 100644 --- a/docs/contribute/api-reference-cpp.mdx +++ b/docs/contribute/api-reference-cpp.mdx @@ -461,12 +461,19 @@ publish the reference automatically. Doxybook2-based pipeline). Add a job with a `doxygen2docusaurus` field (instead of `from`) to that -submodule's list in `scripts/external-jobs.js`: +submodule's list in `scripts/external-jobs.js`. If the tool is under +[per-tool documentation versioning](./versioning) (every current +doxygen2docusaurus consumer on this site is — see the real `2f85_cpp` job), +it also needs `destRoot: 'versioned-tools'` and `routeBasePath` — see the +comment on both, and on `apiFolderPath`/`docsBaseUrl` below, for why a +`destRoot` job needs `routeBasePath` spelled out explicitly: ```js { doxygen2docusaurus: { doxyfileDir: 'sdk_cpp' }, - to: 'drivers/Adaptive grippers/Libraries/C++/API', + to: 'Adaptive grippers/Libraries/C++/API', + destRoot: 'versioned-tools', + routeBasePath: '/docs/drivers/Adaptive grippers/Libraries/C++', exclude: [ 'files', 'folders', 'indices/files', 'namespaces', 'indices/namespaces', @@ -475,6 +482,10 @@ submodule's list in `scripts/external-jobs.js`: }, ``` +A non-versioned tool's job (no `destRoot`) skips `routeBasePath` entirely +and uses `to: 'drivers/Adaptive grippers/Libraries/C++/API'` instead — +site-relative from `docs/`, the same as any other job's `to`. + `sync-external-docs.js`'s handling of a `doxygen2docusaurus` job first deletes that submodule's `doxygen-xml/` output folder, then runs `doxygen`. **Doxygen never cleans its own `OUTPUT_DIRECTORY` between runs** — an XML @@ -489,18 +500,31 @@ exclusion just sat there, untouched, until something finally deleted the folder. It then writes a `doxygen2docusaurus.json` config next to the repo root -(`apiFolderPath`/`apiBaseUrl` both set to the job's own `to`, so every -generated slug and cross-reference is already correct for where the content -ends up — no post-hoc link rewriting needed) and runs the tool against it, -staged into a gitignored folder rather than straight into `docs/` (doxygen2docusaurus -also wipes its own *output* folder on every run — staging keeps that -separate from the post-processing pipeline below, and lets `pruneStale` -compare "what should exist now" against what's actually in `docs/` before -touching anything there). It then applies the transforms described in +and runs the tool against it, staged into a gitignored folder rather than +straight into `docs/`/`versioned-tools/` (doxygen2docusaurus also wipes its +own *output* folder on every run — staging keeps that separate from the +post-processing pipeline below, and lets `pruneStale` compare "what should +exist now" against what's actually there before touching anything). For a +**non-versioned** job, `apiFolderPath`/`apiBaseUrl` are both just the job's +own `to` (this site's real mount point), so every generated slug and +cross-reference is already correct for where the content ends up — no +post-hoc link rewriting needed. For a **versioned** (`destRoot`) job, +`apiFolderPath` is reduced to just the instance-relative tail (e.g. `'API'`, +via `path.basename(job.to)`) since that's also what drives the sidebar +JSON's own doc ids, which must be instance-relative to match +`sidebars..js`; `docsBaseUrl` is set from the job's own +`routeBasePath` instead of the default `'docs'`, so the absolute +`` backlinks doxygen2docusaurus bakes directly into the +generated HTML still resolve to the versioned instance's real +`routeBasePath`, not the default instance's `/docs`. See the big comment on +`destRoot`/`currentDocsRoot`/`currentRoutePrefix` near the top of +`sync-external-docs.js` for the full mechanics. Either way, it then applies +the transforms described in [Matching Doxygen's own reference look](#matching-doxygens-own-reference-look) -above, copies the result into `docs/${to}/`, builds the [Global -Index](#global-index), and writes the pruned/merged sidebar JSON — all -before `sidebars.js` ever reads it back. +above, copies the result into place, builds the [Global +Index](#global-index), and writes the pruned/merged sidebar JSON to +`scripts/generated/doxygen-sidebar--.json` — all before +`sidebars.js`/`sidebars..js` ever reads it back. **Every page under this job comes from the submodule alone — nothing here is hand-authored on this site**, including the section's own landing page: @@ -515,12 +539,22 @@ to update it (this exact page went stale within days, listing a group that had already been renamed upstream), where the generated page is correct by construction on every run. -Add `'drivers/Adaptive grippers/Libraries/C++/API/index'` (Docusaurus resolves a doc's `id` -from its path, regardless of whether the underlying file is `.md` or `.mdx`) -to the sidebar; everything under it comes from -`loadDoxygenSidebarItems('drivers/Adaptive grippers/Libraries/C++/API')` as described in +Add the API reference's `index` doc (Docusaurus resolves a doc's `id` from +its path, regardless of whether the underlying file is `.md` or `.mdx`) to +the sidebar; everything under it comes from `loadDoxygenSidebarItems(...)` +as described in [The sidebar mirrors the Doxygen group hierarchy automatically](#the-sidebar-mirrors-the-doxygen-group-hierarchy-automatically) -above. +above. For a **non-versioned** tool that's a site-relative doc id in +`sidebars.js`, e.g. `'drivers/Adaptive grippers/Libraries/C++/API/index'`, +passed to `loadDoxygenSidebarItems('drivers/Adaptive grippers/Libraries/C++/API')` +(the same site-relative path as the job's own `to`). For a **versioned** +tool it's instance-relative instead, in that tool's own +`sidebars..js` — e.g. `'API/index'` passed to +`loadDoxygenSidebarItems('API')` (the job's own `to` with `destRoot` +stripped) — see `sidebars.adaptive-grippers-cpp.js`'s `doxygenApiCategory` +call for the real, current example, and +[Per-tool documentation versioning](./versioning) for the rest of what's +different about a versioned tool's sidebar. ## Optional: full-fidelity preview on this site @@ -620,6 +654,11 @@ the **tool repo**, then adding one job entry **here** — nothing in plain `{ from, to }` jobs in the same `submoduleJobs(...)` call — see [Splitting a tool page into overview, API reference, and guides](./how-it-works#splitting-a-tool-page-into-overview-api-reference-and-guides) in "How it works". + If the tool is also getting [per-tool documentation versioning](./versioning) + (the common case — see that page's checklist for the rest of the setup), + `destRoot`/`routeBasePath` go on this job too, per + [Wiring it into this site's build](#wiring-it-into-this-sites-build) + above. 9. Add the new section's sidebar entries — the tool's `index.mdx`, the generated API reference's doc id, and the `docs/` guides' doc id, if present — per the same "Splitting a tool page..." section. diff --git a/docs/contribute/how-it-works.mdx b/docs/contribute/how-it-works.mdx index 4915458..65a6be8 100644 --- a/docs/contribute/how-it-works.mdx +++ b/docs/contribute/how-it-works.mdx @@ -35,23 +35,17 @@ robotiq.github.io/ │ └── drivers/ │ ├── Tactile Sensor/ │ │ ├── index.mdx ← product landing page (in git) -│ │ ├── Libraries/ -│ │ │ ├── C++/ -│ │ │ │ ├── index.mdx ← wrapper page (in git) -│ │ │ │ ├── _readme.md ← synced from submodule -│ │ │ │ ├── API/ ← generated API reference (Doxygen + -│ │ │ │ │ └── index.mdx doxygen2docusaurus), see below -│ │ │ │ └── docs/ ← synced copy of the submodule's own -│ │ │ │ └── index.mdx docs/ folder (guides, design notes) -│ │ │ └── Python/ -│ │ │ ├── index.mdx -│ │ │ └── _readme.md +│ │ ├── Libraries/ ← C++ and Python are VERSIONED tools +│ │ │ (see versioned-tools/ below) — this +│ │ │ folder holds no tool pages, only +│ │ │ the sidebar link to each │ │ ├── ROS/ │ │ │ ├── index.mdx ← ROS landing page (in git), holds │ │ │ │ the per-distro compatibility table │ │ │ └── ROS2-Jazzy/index.mdx │ │ └── Simulation/ -│ │ └── Isaac Sim/index.mdx +│ │ └── Isaac Sim/index.mdx ← NOT versioned (own Isaac Sim page, +│ │ distinct from Adaptive grippers') │ ├── Force Torque Sensor/ │ │ ├── index.mdx │ │ ├── Libraries/C/ … @@ -61,8 +55,9 @@ robotiq.github.io/ │ │ └── ROS2-Humble/ … │ └── Adaptive grippers/ │ ├── index.mdx -│ ├── Libraries/C++/ … -│ ├── Libraries/Python/ … +│ ├── Libraries/ ← C++ is a VERSIONED tool (below); +│ │ Python isn't yet +│ │ └── Python/ … │ ├── ROS/ ← newest to oldest, per generation │ │ ├── index.mdx │ │ ├── ROS2-Lyrical/ … @@ -71,20 +66,55 @@ robotiq.github.io/ │ │ ├── ROS1-Melodic/ … │ │ ├── ROS1-Kinetic/ … │ │ └── ROS1-Indigo/ … -│ ├── Simulation/Isaac Sim/ … │ ├── Simulation/PyBullet/ … │ └── Other/GraspGen/ … ← "Other" category +├── versioned-tools/ ← content for VERSIONED tools (own +│ │ Docusaurus plugin instance each, +│ │ Latest/Stable/Previous versions — +│ │ see contribute/versioning.mdx), +│ │ mirrors docs/drivers/'s own shape +│ │ one level in from +│ ├── Tactile Sensor/ +│ │ └── Libraries/ +│ │ ├── C++/ +│ │ │ ├── index.mdx ← wrapper page (in git) +│ │ │ ├── _readme.md ← synced from submodule +│ │ │ ├── API/ ← generated API reference (Doxygen + +│ │ │ │ └── index.mdx doxygen2docusaurus), see below +│ │ │ └── docs/ ← synced copy of the submodule's own +│ │ │ └── index.mdx docs/ folder (guides, design notes) +│ │ └── Python/ +│ │ ├── index.mdx +│ │ └── _readme.md +│ └── Adaptive grippers/ +│ ├── Libraries/C++/ … ← same guides+API shape as above +│ └── Simulation/Isaac Sim/ … ← single page, Latest only so far ├── draft/ ← content with no nav link yet (never │ built into a page — outside docs/) ├── scripts/ │ ├── sync-external-docs.js -│ └── generate-tools-table.js -└── sidebars.js +│ ├── generate-tools-table.js +│ └── site-nav-tree.mjs ← shared nav tree, see +│ contribute/versioning.mdx +├── sidebars.js ← main instance's sidebar +└── sidebars..js ← one per versioned tool ``` Files prefixed with `_` are Docusaurus **partials** — imported by a wrapper `.mdx` but not standalone pages themselves. +**A tool listed above as "VERSIONED" lives under `versioned-tools/`, not +`docs/`, and has its own Docusaurus plugin instance with its own +Latest/Stable/Previous versions switcher** — see +[Per-tool documentation versioning](./versioning) for the whole mechanism, +why it needs a separate top-level folder rather than living inside +`docs/`, and how to add it to a new tool. Everything else on this page +(the sync script, the exclude-content markers, splitting a tool into +guides/API/overview, refreshing submodule pins) applies identically to a +versioned or non-versioned tool — versioning only changes *where* the +files live and *how many builds* exist of them, not how they're +authored or synced. + The `Libraries/`, `ROS/`, `Simulation/`, and `Other/` folders are just organizational containers — `generate-tools-table.js` doesn't care about folder names, only about which `index.mdx` files carry a `Category` badge @@ -151,7 +181,13 @@ A tool's `_readme.md` partial is meant to stay a short overview. Once a repository also has a generated API reference and its own prose documentation (design notes, guides — more than fits in a README), don't cram all three into one page. Split them into sibling folders next to the -tool's `index.mdx`, as with `docs/drivers/Adaptive grippers/Libraries/C++/`: +tool's `index.mdx`. The example below is written against a plain, +non-versioned tool under `docs/`; the real worked example on this site, +`versioned-tools/Adaptive grippers/Libraries/C++/`, is a *versioned* tool +(see [Per-tool documentation versioning](./versioning)) — same folder +shape, but under `versioned-tools/` instead of `docs/`, and with a +different sidebar entry (`sidebars.adaptive-grippers-cpp.js`'s `cppItem`, +not a `sidebars.js` entry): - **`/index.mdx`** — the manually maintained overview (unchanged); still imports `_readme.md`. No need to link to the two folders below by @@ -168,7 +204,7 @@ tool's `index.mdx`, as with `docs/drivers/Adaptive grippers/Libraries/C++/`: `scripts/external-jobs.js`: ```js - { from: 'docs', to: 'drivers/Adaptive grippers/Libraries/C++/docs' }, + { from: 'docs', to: 'drivers////docs' }, ``` **Name the source repo's guide files `NN-kebab-case.md`** (e.g. @@ -193,11 +229,11 @@ becomes a category instead of a single page: ```js { type: 'category', - label: 'C++', - link: { type: 'doc', id: 'drivers/Adaptive grippers/Libraries/C++/index' }, + label: '', + link: { type: 'doc', id: 'drivers////index' }, items: [ - 'drivers/Adaptive grippers/Libraries/C++/API/index', - 'drivers/Adaptive grippers/Libraries/C++/docs/index', + 'drivers////API/index', + 'drivers////docs/index', ], }, ``` @@ -269,3 +305,34 @@ already covers each half: repo's Insights → Dependency graph page, or the auto-merge workflow re-runs automatically on its own `pull_request_target` trigger for any Dependabot PR. + +## Manually refreshing the deployed site + +The site updates itself once a day: Dependabot checks every tool repo at +around 00:45 UTC, and any bump it opens is auto-merged and deployed (see +[Refreshing submodule pins automatically](#refreshing-submodule-pins-automatically)). +A change pushed to a tool repo after that check only reaches the site at +the next one, up to a day later. + +Dependabot's default 3-day cooldown is switched off in +`.github/dependabot.yml` (`cooldown.exclude: ["*"]`). With the default, +any tool-repo commit younger than 3 days is ignored, even by **Check for +updates** below, so a docs change took 3 to 4 days to go live. + +To run that same daily update right away from the GitHub web interface: + +1. Open the repository on GitHub and go to **Insights → Dependency graph → + Dependabot** + ([direct link](https://github.com/robotiq/robotiq.github.io/network/updates)). +2. On the `gitsubmodule` entry, click **Check for updates**. + +Dependabot opens one bump PR per submodule that is behind its repo's +default branch. Each one auto-merges once `build-and-test` passes, and +each merge deploys the site, usually within 10 minutes. You can follow +the PRs in the **Pull requests** tab and the deploys in the **Actions** +tab. If no PR appears, every pin was already up to date. + +Re-running **Deploy to GitHub Pages** from the **Actions** tab doesn't +pick up new tool-repo content: it rebuilds from the pins already on +`main`. It's only useful for republishing the current state, for example +after a failed deploy. diff --git a/docs/contribute/index.mdx b/docs/contribute/index.mdx index 4a804e6..1509063 100644 --- a/docs/contribute/index.mdx +++ b/docs/contribute/index.mdx @@ -11,6 +11,7 @@ a pull request, and embedding a software tool repository as a Git submodule with optional auto-generated API docs. It's split by topic — see the sidebar for the rest ([How it works](./how-it-works), [Adding a new software tool](./adding-a-tool), +[Per-tool documentation versioning](./versioning), [Auto-generated software tools tables](./tools-tables), [Auto-generated API reference](./api-reference-cpp), and a [Quick reference](./quick-reference) table). @@ -55,7 +56,9 @@ npm start ``` Opens `http://localhost:3000` in your browser. The page hot-reloads -when you edit files under `docs/`, `src/`, or `sidebars.js`. +when you edit files under `docs/`, `src/`, `sidebars.js`, or, for a +versioned tool (see [Per-tool documentation versioning](./versioning)), +`versioned-tools/`, `sidebars..js`, or `scripts/site-nav-tree.mjs`. ### Previewing local edits to a submodule @@ -107,9 +110,9 @@ pick up new commits — terminal 1's dev server hot-reloads the result, no restart needed. Skip to step 3. ⚠️ **Exception: adding or removing a Doxygen group (`\defgroup`).** The C++ -API sidebar is built dynamically from whatever files currently exist under -`API/Modules/` (see -[`scripts/doxygen-groups-sidebar.mjs`](https://github.com/robotiq/robotiq.github.io/blob/main/scripts/doxygen-groups-sidebar.mjs)), +API sidebar is read once from `scripts/generated/doxygen-sidebar-.json` +(written by `sync-external-docs.js`'s `doxygen2docusaurus` handling — see +[The sidebar mirrors the Doxygen group hierarchy automatically](./api-reference-cpp#the-sidebar-mirrors-the-doxygen-group-hierarchy-automatically)), but that scan only runs once, when the dev server starts — it isn't re-run on every file change the way doc content is. Editing an existing group's content hot-reloads fine, but a *new* or *deleted* group won't show up in diff --git a/docs/contribute/quick-reference.mdx b/docs/contribute/quick-reference.mdx index 32861bb..fd7b9d4 100644 --- a/docs/contribute/quick-reference.mdx +++ b/docs/contribute/quick-reference.mdx @@ -10,15 +10,17 @@ displayed_sidebar: contributeSidebar |---|---| | Submodules (pinned repo imports) | `external//` | | Sync + generation config | `scripts/external-jobs.js` → `JOBS`, authored via `submoduleJobs(submodule, { repoUrl, branch }, [...])` per submodule | -| C++ API sidebar (auto-built from Doxygen groups) | `scripts/doxygen-groups-sidebar.mjs`, called from `sidebars.js` — mirrors the `\defgroup`/`\ingroup` hierarchy with no manual sidebar entries; see [The sidebar mirrors the Doxygen group hierarchy automatically](./api-reference-cpp#the-sidebar-mirrors-the-doxygen-group-hierarchy-automatically) | +| C++ API sidebar (auto-built from Doxygen groups) | `scripts/generated/doxygen-sidebar--.json`, written by `sync-external-docs.js` and read back by `sidebars.js`/`sidebars..js`'s `loadDoxygenSidebarItems` — mirrors the `\defgroup`/`\ingroup` hierarchy with no manual sidebar entries; see [The sidebar mirrors the Doxygen group hierarchy automatically](./api-reference-cpp#the-sidebar-mirrors-the-doxygen-group-hierarchy-automatically) | +| Per-tool Latest/Stable/Previous versions | one `@docusaurus/plugin-content-docs` instance per versioned tool in `docusaurus.config.js`'s `plugins` array, content under `versioned-tools////` (not `docs/`) — see [Per-tool documentation versioning](./versioning) | +| Full site nav tree, shared by the main sidebar and every versioned tool's own sidebar | `scripts/site-nav-tree.mjs` — see [Per-tool documentation versioning](./versioning) | | One-command preview of pushed WIP submodule commits | `npm run preview` in a **second terminal**, run **before** starting terminal 1 with `SKIP_SUBMODULE_RESET=1 npm start` (plain `npm start` resets the submodule via its own `prestart` and undoes the preview) — **not** `npm start preview`, which mis-parses as a `docusaurus start` site-directory argument — see [Previewing local edits to a submodule](./#previewing-local-edits-to-a-submodule) | | Exclude README content from the synced page | `` ... `` in the tool repo's README | -| Synced partials | `docs/drivers////_readme.md` | -| Wrapper pages | `docs/drivers////index.mdx` | -| Generated API reference folder | `docs/drivers////API/` | +| Synced partials | `docs/drivers////_readme.md` — or `versioned-tools////_readme.md` for a versioned tool, see [Per-tool documentation versioning](./versioning) | +| Wrapper pages | `docs/drivers////index.mdx` (`versioned-tools/...` for a versioned tool) | +| Generated API reference folder | `docs/drivers////API/` (`versioned-tools/...` for a versioned tool) | | Compile-checked C++ examples | `\snippet ` in a doc comment, tagged in a real file under `sdk_cpp/examples/` — not a hand-written `\code` block. C++ only; Python has no equivalent. See [Compile-checked examples with `\snippet`](./api-reference-cpp#compile-checked-examples-with-snippet) | | Verified code examples in guide pages (outside Doxygen's reach) | `` above a fenced code block; checked by `check_doc_snippets.py` in both the tool repo's own CI and here via `scripts/check-doc-snippets.js` + `docSnippetsCheck` in `external-jobs.js`. Template at `templates/check_doc_snippets.py`. See [Verifying markdown code examples against real source](./api-reference-cpp#verifying-markdown-code-examples-against-real-source) | -| Synced guide docs folder (verbatim copy of the repo's own `docs/`) | `docs/drivers////docs/` | +| Synced guide docs folder (verbatim copy of the repo's own `docs/`) | `docs/drivers////docs/` (`versioned-tools/...` for a versioned tool) | | Category badge (Libraries / ROS1 / ROS2 / Simulation / Other) | `![...](.../Category--lightgrey)` at the top of each wrapper page | | Support badge | `![Supported by ` — `src/remark/externalLinksNewTab.mjs`; no `target="_blank"` needed by hand | | A YouTube video embed | write a thumbnail image wrapped in a link to the same video (`[![Title](https://img.youtube.com/vi//...)](https://youtu.be/)`) — GitHub-safe, and auto-upgraded to a real player by `src/remark/youtubeEmbed.mjs`; no raw ` output, where before the .mdx one failed to build at all. Co-Authored-By: Claude Sonnet 5 --- src/remark/youtubeEmbed.mjs | 81 ++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/src/remark/youtubeEmbed.mjs b/src/remark/youtubeEmbed.mjs index 61dd536..d578cb1 100644 --- a/src/remark/youtubeEmbed.mjs +++ b/src/remark/youtubeEmbed.mjs @@ -18,21 +18,75 @@ // it happens to link somewhere" paragraph never matches both conditions on // the same id. // -// Emits a raw mdast `html` node rather than an MDX JSX element: unlike -// src/remark/robotiqWordmark.mjs (opt-in markup only ever hand-authored in -// this site's own .mdx pages), this plugin also runs over synced docs -// rendered in plain-CommonMark mode (see markdown.format: 'detect' in -// docusaurus.config.js) — a raw `html` node is exactly what mdast already -// uses for the hand-written raw HTML (``, `
`, ...) those pages -// rely on, and flows through the same remark-rehype + rehype-raw path -// Docusaurus already runs for both plain Markdown and MDX. +// Emits a DIFFERENT node depending on whether the file being compiled is +// MDX or plain Markdown (see markdown.format: 'detect' in +// docusaurus.config.js, which decides this per file, by extension) — +// MDX and plain-Markdown pages go through different compilers, and a node +// valid for one throws in the other: +// - Plain Markdown (a synced .md guide): a raw mdast `html` node. That's +// exactly what mdast already uses for the hand-written raw HTML +// (`
`, `
`, ...) those pages rely on, and Docusaurus's +// plain-Markdown pipeline runs rehype-raw, which is what lets it (and +// this plugin's output) through to real HTML. +// - MDX (a hand-authored .mdx page): an `mdxJsxFlowElement` — the +// JSX-flavoured node MDX's own compiler expects in place of a `paragraph`. +// Docusaurus does NOT run rehype-raw for MDX, so a raw `html` node there +// fails the build with "Cannot handle unknown node `raw`" — an earlier +// version of this plugin emitted `html` unconditionally, which worked for +// every synced .md guide tested but would have broken the first .mdx page +// to actually use this pattern (caught in review before that happened). import {visit} from 'unist-util-visit'; const THUMBNAIL_RE = /^https:\/\/img\.youtube\.com\/vi\/([\w-]+)\//; const VIDEO_LINK_RE = /(?:youtu\.be\/|youtube\.com\/watch\?v=)([\w-]+)/; +const IFRAME_ALLOW = + 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; + +function buildHtmlNode(thumbnailId, title) { + // Raw HTML text, so the title has to be escaped by hand — this is the + // only one of the two node shapes where that's true (the MDX attribute + // below is a plain AST string value, not text mdast-util-mdx-jsx has to + // parse back out of markup, so it needs no escaping at all). + const escapedTitle = title.replace(/"/g, '"'); + return { + type: 'html', + value: `
`, + }; +} + +function mdxAttr(name, value) { + return {type: 'mdxJsxAttribute', name, value}; +} + +function buildMdxNode(thumbnailId, title) { + return { + type: 'mdxJsxFlowElement', + name: 'div', + attributes: [mdxAttr('className', 'video-wrapper')], + children: [ + { + type: 'mdxJsxFlowElement', + name: 'iframe', + attributes: [ + mdxAttr('src', `https://www.youtube.com/embed/${thumbnailId}`), + mdxAttr('title', title), + mdxAttr('allow', IFRAME_ALLOW), + // A bare, valueless attribute is JSX's own boolean-prop shorthand + // (``, - }; + const title = image.alt || 'Video'; + parent.children[index] = isMdx + ? buildMdxNode(thumbnailId, title) + : buildHtmlNode(thumbnailId, title); }); }; } From 260a153e54c330d31ca238435b87189e429ae867 Mon Sep 17 00:00:00 2001 From: bcastets-robotiq Date: Fri, 25 Sep 2026 15:05:16 -0400 Subject: [PATCH 3/5] Fix 4 review findings from PR #14 + add a unit test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses every inline comment from review, each reproduced before fixing: - scripts/list-submodule-tags.js: an annotated tag's SHA was the tag object's own SHA, not the commit it points at (git ls-remote emits both; the `^{}` peeled line was being dropped instead of preferred). A Stable pin or Previous-versions link built from this pointed at the wrong object. Parsing logic extracted into a pure `parseLsRemote` for testing. - src/remark/youtubeEmbed.mjs: emitted a raw mdast `html` node unconditionally, which Docusaurus can't turn into HTML on an `.mdx` page (no rehype-raw there) — would fail any `.mdx` page hitting the thumbnail-link pattern with "Cannot handle unknown node `raw`". Now emits an `mdxJsxFlowElement` unconditionally instead, which compiles correctly for both `.md` (via rehype-raw's own passThrough list) and `.mdx` — one node shape, no format branching needed, and no manual HTML-escaping either. - scripts/site-nav-tree.mjs: `buildInstanceSidebar`'s full output gets frozen into every cut version's `*_versioned_sidebars/*.json` snapshot, so a later change to the shared site nav tree (renamed label, moved page, new product) never reached an already-cut version. Added `extractActiveItem`/`regenerateInstanceSidebar` plus scripts/regenerate-versioned-sidebars.mjs, wired into `npm run generate`, so every snapshot is kept in sync automatically and ci.yml's existing drift check now covers these files too. - scripts/sync-external-docs.js: a job that gained `destRoot` (moved its output from docs/ to versioned-tools/) left its OLD output under docs/drivers/ untouched on a checkout that had already synced against an older commit — reproduced as 49 duplicate-route warnings on a real build. Added cleanupLegacyDestRoot, run for every destRoot job (including the doxygen2docusaurus one), which prunes that legacy location on every sync going forward. Also adds a `node:test`-based unit test harness (`npm run test:unit`, wired into ci.yml before Build) covering all four fixes plus a drift-guard regression test for the versioned-sidebar staleness issue. pruneStale/cleanupLegacyDestRoot moved into scripts/lib/prune.js so they're importable/testable without running the full sync pipeline. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 18 +++- package.json | 5 +- scripts/lib/prune.js | 118 ++++++++++++++++++++++ scripts/list-submodule-tags.js | 41 ++++++-- scripts/regenerate-versioned-sidebars.mjs | 67 ++++++++++++ scripts/site-nav-tree.mjs | 71 +++++++++++++ scripts/sync-external-docs.js | 44 ++------ src/remark/youtubeEmbed.mjs | 85 +++++++--------- test/list-submodule-tags.test.js | 67 ++++++++++++ test/prune.test.js | 111 ++++++++++++++++++++ test/site-nav-tree.test.mjs | 103 +++++++++++++++++++ test/youtube-embed.test.mjs | 104 +++++++++++++++++++ 12 files changed, 741 insertions(+), 93 deletions(-) create mode 100644 scripts/lib/prune.js create mode 100644 scripts/regenerate-versioned-sidebars.mjs create mode 100644 test/list-submodule-tags.test.js create mode 100644 test/prune.test.js create mode 100644 test/site-nav-tree.test.mjs create mode 100644 test/youtube-embed.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fa06ac..6125090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,16 @@ jobs: - name: Install Doxygen and Graphviz run: sudo apt-get update && sudo apt-get install -y doxygen graphviz + # Pure logic, no build/network/submodule checkout needed — runs before + # Build so a failure here is fast and doesn't waste a Doxygen run. + # See test/ for what's covered: list-submodule-tags.js's tag parsing, + # sync-external-docs.js's stale-file pruning (scripts/lib/prune.js), + # the YouTube-embed remark plugin (both .md and .mdx compilation — + # regression coverage for the bug in PR #14's review), and + # site-nav-tree.mjs's versioned-sidebar drift guard (below). + - name: Unit tests + run: npm run test:unit + # Fails on broken internal links / anchors (onBrokenLinks: 'throw') # and runs the sync + tools-table generators via the prebuild hook. - name: Build @@ -69,6 +79,12 @@ jobs: # a per-tool-versioned tool's own wrapper page (see # docs/contribute/versioning.mdx) — same gitignore split there # (versioned-tools/**/*.md), so this check covers both directories. + # regenerate-versioned-sidebars.mjs (also run via the prebuild hook, + # as part of `npm run generate`) does the same for every + # `*_versioned_sidebars/*.json` snapshot — the unit-test drift guard + # above already covers this statically, but a real build regenerating + # something the static check couldn't see (e.g. a doc's frontmatter + # label actually changing) still needs this line to catch it. # Nothing else regenerates and commits these after a merge, so a PR # that changes something the generator depends on (e.g. a Dependabot # submodule bump) without also re-running it locally would otherwise @@ -76,7 +92,7 @@ jobs: # This catches that here, generically — no file list to keep in sync # as the generator's own output grows. - name: Verify generated content is committed - run: git diff --exit-code -- docs/ versioned-tools/ + run: git diff --exit-code -- docs/ versioned-tools/ '*_versioned_sidebars/' # Smoke-tests the generated output (key pages + tools table present). - name: Test diff --git a/package.json b/package.json index e2e2d56..d1269aa 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "generate": "node scripts/sync-external-docs.js && node scripts/check-doc-snippets.js && node scripts/generate-tools-table.js", - "preview": "node scripts/preview-external-docs.js && node scripts/check-doc-snippets.js && node scripts/generate-tools-table.js", + "generate": "node scripts/sync-external-docs.js && node scripts/check-doc-snippets.js && node scripts/generate-tools-table.js && node scripts/regenerate-versioned-sidebars.mjs", + "preview": "node scripts/preview-external-docs.js && node scripts/check-doc-snippets.js && node scripts/generate-tools-table.js && node scripts/regenerate-versioned-sidebars.mjs", "prestart": "npm run generate", "start": "cross-env NODE_OPTIONS=--max-old-space-size=8192 docusaurus start", "prebuild": "npm run generate", "build": "docusaurus build", + "test:unit": "node --test \"test/**/*.test.js\" \"test/**/*.test.mjs\"", "test": "node scripts/check-build.js", "swizzle": "docusaurus swizzle", "clear": "docusaurus clear", diff --git a/scripts/lib/prune.js b/scripts/lib/prune.js new file mode 100644 index 0000000..d7d0681 --- /dev/null +++ b/scripts/lib/prune.js @@ -0,0 +1,118 @@ +// Filesystem-pruning helpers used by sync-external-docs.js, extracted into +// their own module so they can be unit tested against a throwaway temp +// directory (fs.mkdtempSync) without running the full sync pipeline — that +// needs real submodule checkouts, network access, and the `doxygen` +// binary. See test/prune.test.js. +const fs = require('fs'); +const path = require('path'); + +const COPY_EXTS = new Set(['.md', '.mdx', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.pdf']); + +// A folder job only ever adds/overwrites — if the source repo renames or +// removes a file, the old copy would otherwise linger in docs/ forever and +// get picked up as a stale, duplicate sidebar entry (see +// scripts/folder-sidebar.mjs, which lists every file actually present on +// disk). Called once per folder job's destDir after all JOBS have run, so +// files written by an unrelated job into the same tree (e.g. the register- +// map file job writing into a folder job's API/Modules/) are already in +// `written` and don't get flagged as stale. +// `index`/`README` are exempt — those are this site's own hand-authored +// landing pages for the folder, never synced from source (see "Splitting a +// tool page into overview, API reference, and guides" in +// docs/contribute/how-it-works.mdx). +// Returns the absolute paths it actually removed, so a caller can log them +// (e.g. relative to its own ROOT) without this module needing to know +// anything about the caller's own path conventions. +function pruneStale(destDir, written, removed = []) { + if (!fs.existsSync(destDir)) return removed; + for (const entry of fs.readdirSync(destDir, { withFileTypes: true })) { + const child = path.join(destDir, entry.name); + if (entry.isDirectory()) { + pruneStale(child, written, removed); + if (fs.readdirSync(child).length === 0) fs.rmdirSync(child); + continue; + } + if (!COPY_EXTS.has(path.extname(entry.name).toLowerCase())) continue; + const base = entry.name.replace(/\.[^.]+$/, ''); + if (base === 'index' || base === 'README') continue; + if (!written.has(child)) { + fs.unlinkSync(child); + removed.push(child); + } + } + return removed; +} + +// A job with `destRoot` writes its output somewhere other than `docs/` +// (see the comment on `destRoot` in sync-external-docs.js) — but +// `pruneStale` only ever walks a job's *current* destPath, so it has no +// way to know about that same job's OLD output from before it gained a +// destRoot. A checkout that already ran this script once against an +// older commit (e.g. `main`, before this job moved) still has that old +// output sitting on disk, untracked and gitignored — `git checkout` +// doesn't touch gitignored files — so switching to a branch where the job +// now has a destRoot leaves both the old and new copies in place, and the +// main docs plugin instance keeps serving the stale one at the exact same +// public URL the job's own new instance now also serves. Docusaurus +// reports that as a duplicate route ("This could lead to non-deterministic +// routing behavior") rather than silently picking one — confirmed +// reproducible: sync on `main`, switch branches, `npm run build`, 49 +// duplicate routes. A fresh clone or CI checkout never has this legacy +// output in the first place, so this only matters for a local checkout +// that's been sitting across the switch — but sync runs on every build, +// so cleaning it up here catches it automatically, no manual `rm -rf` +// needed once. Safe to remove once every job that will ever gain a +// destRoot already has one. +// +// The legacy path is NOT `docs/` — `job.to` itself was shortened +// when destRoot was introduced (it dropped its leading `drivers/` +// segment, since the tool's own new plugin instance's `routeBasePath` +// already supplies everything up to the product root — see the +// `destRoot` comment in sync-external-docs.js and docusaurus.config.js's +// `plugins` array). Every destRoot job today is a product/tool under +// `docs/drivers/` before the move, so the legacy path is +// `docs/drivers/`. Found this the hard way: the first version of +// this function used `path.join(rootDir, 'docs', job.to)` (matching the +// shape of the review comment that flagged this bug) and silently cleaned +// up nothing at all — verified by re-creating the exact stale files the +// reproduction steps described and confirming this version actually +// removes them. +// Deliberately NOT pruneStale's own index/README exemption: that one +// exists to protect a hand-authored `index.mdx` sitting next to synced +// siblings in the *live*, current destPath. This folder is pure legacy +// output nothing authors into any more, and this codebase's own +// convention (see .gitignore's `docs/drivers/**/*.md` rule) is that a +// `.md` file anywhere under `docs/drivers/` is always synced/generated, +// never hand-authored — including one literally named `index.md` +// (doxygen2docusaurus's own API landing page uses exactly that name). +// Only a genuine `.mdx` survives here. +function pruneLegacyFolder(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const child = path.join(dir, entry.name); + if (entry.isDirectory()) { + pruneLegacyFolder(child); + if (fs.existsSync(child) && fs.readdirSync(child).length === 0) fs.rmdirSync(child); + continue; + } + if (path.extname(entry.name).toLowerCase() === '.mdx') continue; + fs.unlinkSync(child); + } +} + +function cleanupLegacyDestRoot(rootDir, job) { + if (!job.destRoot) return; + const legacyPath = path.join(rootDir, 'docs', 'drivers', job.to); + if (!fs.existsSync(legacyPath)) return undefined; + if (fs.statSync(legacyPath).isDirectory()) { + pruneLegacyFolder(legacyPath); + if (fs.existsSync(legacyPath) && fs.readdirSync(legacyPath).length === 0) { + fs.rmdirSync(legacyPath); + } + } else { + fs.unlinkSync(legacyPath); + } + return path.join('docs', 'drivers', job.to); +} + +module.exports = { COPY_EXTS, pruneStale, pruneLegacyFolder, cleanupLegacyDestRoot }; diff --git a/scripts/list-submodule-tags.js b/scripts/list-submodule-tags.js index 12d5394..ae33ee6 100644 --- a/scripts/list-submodule-tags.js +++ b/scripts/list-submodule-tags.js @@ -34,25 +34,44 @@ function compareSemver(a, b) { return 0; } -// Returns [{ name, commit }], newest first. Skips non-semver tags and the -// '^{}' dereference lines `git ls-remote` emits for annotated tags (those -// point at the tag object itself, not the commit — 2f85_cpp's v1.0.0 is -// one, confirmed against its actual ls-remote output). -function listTags(repoUrl) { - const output = execFileSync('git', ['ls-remote', '--tags', repoUrl], { encoding: 'utf8' }); - const tags = []; +// Parses raw `git ls-remote --tags` output into [{ name, commit }], newest +// first. Pure/side-effect-free (no shelling out) so it can be unit tested +// directly against captured output — see test/list-submodule-tags.test.js. +// +// For an ANNOTATED tag, `git ls-remote` prints two lines: `refs/tags/vX` +// holds the tag OBJECT's own SHA, and `refs/tags/vX^{}` holds the SHA of +// the commit it peels to — the one a Stable pin or source link actually +// needs. A lightweight tag has only the first line, and that line's SHA +// already is the commit. Confirmed against a real annotated tag +// (robotiq/grippers' v1.0.0): the plain line's SHA is a `tag` object per +// `git cat-file -t`, the `^{}` line's SHA is the `commit` it points at. +// Bug found and fixed after review (see PR #14): an earlier version kept +// whichever line came first and dropped the `^{}` line outright — for an +// annotated tag that silently pinned Stable to the wrong object. +function parseLsRemote(output) { + const byName = new Map(); for (const line of output.split('\n')) { const match = line.match(/^(\S+)\s+refs\/tags\/(\S+)$/); if (!match) continue; - const [, commit, name] = match; - if (name.endsWith('^{}')) continue; + const [, commit, ref] = match; + const peeled = ref.endsWith('^{}'); + const name = peeled ? ref.slice(0, -3) : ref; if (!SEMVER_TAG_RE.test(name)) continue; - tags.push({ name, commit }); + // A peeled line always wins (it's the commit); a lightweight tag's + // single line is only kept if nothing has claimed this name yet. + if (peeled || !byName.has(name)) byName.set(name, commit); } + const tags = [...byName].map(([name, commit]) => ({ name, commit })); tags.sort((a, b) => compareSemver(a.name, b.name)); return tags; } +// Returns [{ name, commit }], newest first, for a submodule's real remote. +function listTags(repoUrl) { + const output = execFileSync('git', ['ls-remote', '--tags', repoUrl], { encoding: 'utf8' }); + return parseLsRemote(output); +} + function reposFromJobs() { const seen = new Map(); for (const job of JOBS) { @@ -78,4 +97,4 @@ if (require.main === module) { } } -module.exports = { listTags, compareSemver, SEMVER_TAG_RE }; +module.exports = { listTags, parseLsRemote, compareSemver, SEMVER_TAG_RE }; diff --git a/scripts/regenerate-versioned-sidebars.mjs b/scripts/regenerate-versioned-sidebars.mjs new file mode 100644 index 0000000..4e76bb0 --- /dev/null +++ b/scripts/regenerate-versioned-sidebars.mjs @@ -0,0 +1,67 @@ +// @ts-check + +// Regenerates every already-cut version's frozen sidebar snapshot +// (_versioned_sidebars/version--sidebars.json) from the live +// scripts/site-nav-tree.mjs, so a later change to the shared site nav tree +// (new product, renamed label, moved/removed page) doesn't leave an +// already-cut version holding a stale copy of the OLD tree forever — see +// docs/contribute/versioning.mdx and the big comment on +// extractActiveItem/regenerateInstanceSidebar in site-nav-tree.mjs for how +// this correctly regenerates a version WITHOUT needing to already know +// what real content shape that specific version's active tool has (which +// can differ from the tool's own *current* version). +// +// Run as part of `npm run generate`, so it's always fresh before a build; +// ci.yml's "Verify generated content is committed" step then catches +// drift here the same way it already does for docs/ and versioned-tools/. +// +// A tool discovered by its `_versioned_sidebars/` folder name — no +// hardcoded tool list to keep in sync as more tools gain versioning. +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { regenerateInstanceSidebar } from './site-nav-tree.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const SUFFIX = '_versioned_sidebars'; + +let changed = 0; + +for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.endsWith(SUFFIX)) continue; + const tool = entry.name.slice(0, -SUFFIX.length); + const dir = path.join(ROOT, entry.name); + + for (const file of fs.readdirSync(dir)) { + if (!file.endsWith('-sidebars.json')) continue; + const filePath = path.join(dir, file); + const raw = fs.readFileSync(filePath, 'utf8'); + const existing = JSON.parse(raw); + const [sidebarKey, existingItems] = Object.entries(existing)[0]; + + const regenerated = regenerateInstanceSidebar(tool, existingItems); + if (regenerated === undefined) { + console.warn( + `[regenerate-versioned-sidebars] Could not find '${tool}'s own content in ${path.relative(ROOT, filePath)} — left unchanged. The site nav tree's shape may have changed structurally since this was last regenerated; regenerate this one by hand.` + ); + continue; + } + + const next = JSON.stringify({ [sidebarKey]: regenerated }, null, 2) + '\n'; + // Compare with line endings normalized — this repo's checked-out files + // are CRLF (Windows git config), this script's own output is LF, and + // git re-normalizes that difference on the next commit regardless, so + // comparing raw bytes would report a "change" on every single run even + // when nothing about the actual sidebar content differs. + if (next.replace(/\r\n/g, '\n') !== raw.replace(/\r\n/g, '\n')) { + fs.writeFileSync(filePath, next); + changed += 1; + console.log(`[regenerate-versioned-sidebars] Updated ${path.relative(ROOT, filePath)}`); + } + } +} + +if (changed === 0) { + console.log('[regenerate-versioned-sidebars] All versioned sidebar snapshots already up to date.'); +} diff --git a/scripts/site-nav-tree.mjs b/scripts/site-nav-tree.mjs index 958bcab..d22909e 100644 --- a/scripts/site-nav-tree.mjs +++ b/scripts/site-nav-tree.mjs @@ -201,3 +201,74 @@ function renderInstance(node, activeTool, activeItem) { export function buildInstanceSidebar(activeTool, activeItem) { return SITE_TREE.map((n) => renderInstance(n, activeTool, activeItem)); } + +// Docusaurus freezes a cut version's sidebar into +// `_versioned_sidebars/version--sidebars.json` at `docs:version:` +// time — it never re-reads sidebars..js for anything but the +// *current* version. That snapshot is `buildInstanceSidebar`'s WHOLE +// output (see docs/contribute/versioning.mdx), so it goes stale the same +// way any other cached copy of SITE_TREE would: a renamed label, a moved +// page, a new product added later, none of that reaches an already-cut +// version until someone notices and hand-edits (or re-cuts) it. +// +// `extractActiveItem` + `regenerateInstanceSidebar` below are how +// scripts/regenerate-versioned-sidebars.mjs keeps every cut version's +// snapshot in sync on every `npm run generate`, without needing to know +// per-tool, per-version which real content shape `activeItem` should be +// (a versioned tool's non-current versions can have a DIFFERENT shape +// than its current one — e.g. adaptive-grippers-cpp's Stable is a single +// page today, sparse content from before its source repo grew a docs/ +// folder, while its Latest has nested guides/API). Rather than guess that +// shape, this walks SITE_TREE in lockstep with the version's OWN existing +// snapshot and pulls out whatever's already sitting at `activeTool`'s +// position — correct by construction, since that position held the real, +// frozen content the moment the version was actually cut, and nothing +// about a tool's own frozen content changes after the fact (only the +// surrounding site tree does). This assumes the existing snapshot is +// STRUCTURALLY parallel to the current SITE_TREE (same shape at every +// other position) — true immediately after any regeneration, including +// this one, so it self-heals on the very next run; it could only miss if +// SITE_TREE's own shape changed AND a version was never regenerated since +// (a one-time gap, not a standing risk, given this runs on every build). +export function extractActiveItem(activeTool, existingItems) { + for (let i = 0; i < SITE_TREE.length; i += 1) { + const node = SITE_TREE[i]; + const existing = existingItems[i]; + if (!existing) continue; + if (node.kind === 'versioned' && node.tool === activeTool) return existing; + if (node.kind === 'category' && Array.isArray(existing.items)) { + const found = extractActiveItemFrom(node.items, existing.items, activeTool); + if (found !== undefined) return found; + } + } + return undefined; +} + +function extractActiveItemFrom(nodes, existingItems, activeTool) { + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + const existing = existingItems[i]; + if (!existing) continue; + if (node.kind === 'versioned' && node.tool === activeTool) return existing; + if (node.kind === 'category' && Array.isArray(existing.items)) { + const found = extractActiveItemFrom(node.items, existing.items, activeTool); + if (found !== undefined) return found; + } + } + return undefined; +} + +/** + * Regenerates one cut version's frozen sidebar snapshot: extracts + * `activeTool`'s real content out of its own existing snapshot + * (`existingItems`, that version's current sidebar array), then rebuilds + * the surrounding tree fresh from the live SITE_TREE. Returns the new + * items array, or `undefined` if `activeTool`'s content couldn't be found + * in `existingItems` (a genuinely new/reshaped tree — falls back to + * leaving that snapshot alone rather than guessing). + */ +export function regenerateInstanceSidebar(activeTool, existingItems) { + const activeItem = extractActiveItem(activeTool, existingItems); + if (activeItem === undefined) return undefined; + return buildInstanceSidebar(activeTool, activeItem); +} diff --git a/scripts/sync-external-docs.js b/scripts/sync-external-docs.js index 0c370ca..7999819 100644 --- a/scripts/sync-external-docs.js +++ b/scripts/sync-external-docs.js @@ -1227,6 +1227,8 @@ function runDoxygen2Docusaurus(job, written, folderDestPaths) { // which pruneStale itself always exempts) sidesteps this regardless of // what casing a previous pipeline left behind. const destPath = path.join(ROOT, job.destRoot || 'docs', job.to); + const removedLegacyPath = cleanupLegacyDestRoot(ROOT, job); + if (removedLegacyPath) console.log(`[sync-external-docs] Removed legacy pre-destRoot output: ${removedLegacyPath}`); if (fs.existsSync(destPath)) { for (const entry of fs.readdirSync(destPath, { withFileTypes: true })) { const base = entry.name.replace(/\.[^.]+$/, ''); @@ -1488,7 +1490,10 @@ function collectClassGroupLabels(node, matchedNodes, out) { } const MARKDOWN_EXTS = new Set(['.md', '.mdx']); -const COPY_EXTS = new Set(['.md', '.mdx', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.pdf']); +// pruneStale/pruneLegacyFolder/cleanupLegacyDestRoot moved to +// scripts/lib/prune.js so they can be unit tested (see test/prune.test.js) +// without running this whole file's top-level sync pipeline. +const { COPY_EXTS, pruneStale, cleanupLegacyDestRoot } = require('./lib/prune'); function isAbsoluteHref(href) { return /^(https?:\/\/|mailto:|#|\/)/.test(href); @@ -1712,37 +1717,6 @@ function processFolder(srcDir, destDir, opts, rootSrcDir = srcDir, written = new return written; } -// A folder job only ever adds/overwrites — if the source repo renames or -// removes a file, the old copy would otherwise linger in docs/ forever and -// get picked up as a stale, duplicate sidebar entry (see -// scripts/folder-sidebar.mjs, which lists every file actually present on -// disk). Called once per folder job's destDir after all JOBS have run, so -// files written by an unrelated job into the same tree (e.g. the register- -// map file job writing into a folder job's API/Modules/) are already in -// `written` and don't get flagged as stale. -// `index`/`README` are exempt — those are this site's own hand-authored -// landing pages for the folder, never synced from source (see "Splitting a -// tool page into overview, API reference, and guides" in -// docs/contribute/how-it-works.mdx). -function pruneStale(destDir, written) { - if (!fs.existsSync(destDir)) return; - for (const entry of fs.readdirSync(destDir, { withFileTypes: true })) { - const child = path.join(destDir, entry.name); - if (entry.isDirectory()) { - pruneStale(child, written); - if (fs.readdirSync(child).length === 0) fs.rmdirSync(child); - continue; - } - if (!COPY_EXTS.has(path.extname(entry.name).toLowerCase())) continue; - const base = entry.name.replace(/\.[^.]+$/, ''); - if (base === 'index' || base === 'README') continue; - if (!written.has(child)) { - console.log(`[sync-external-docs] Removing stale: ${path.relative(ROOT, child)}`); - fs.unlinkSync(child); - } - } -} - const written = new Set(); const folderDestPaths = new Set(); @@ -1765,6 +1739,8 @@ for (const job of JOBS) { // bisecting content (irrelevant) and path (moving the exact same files // outside docs/ fixed it) while building the tactile-python pilot. const destPath = path.join(ROOT, job.destRoot || 'docs', job.to); + const removedLegacyPath = cleanupLegacyDestRoot(ROOT, job); + if (removedLegacyPath) console.log(`[sync-external-docs] Removed legacy pre-destRoot output: ${removedLegacyPath}`); if (!fs.existsSync(srcPath)) { console.warn(`[sync-external-docs] Missing: external/${job.submodule}/${job.from} — run: git submodule update --init`); @@ -1795,5 +1771,7 @@ for (const job of JOBS) { } for (const destPath of folderDestPaths) { - pruneStale(destPath, written); + for (const removedPath of pruneStale(destPath, written)) { + console.log(`[sync-external-docs] Removing stale: ${path.relative(ROOT, removedPath)}`); + } } diff --git a/src/remark/youtubeEmbed.mjs b/src/remark/youtubeEmbed.mjs index d578cb1..3088819 100644 --- a/src/remark/youtubeEmbed.mjs +++ b/src/remark/youtubeEmbed.mjs @@ -18,23 +18,23 @@ // it happens to link somewhere" paragraph never matches both conditions on // the same id. // -// Emits a DIFFERENT node depending on whether the file being compiled is -// MDX or plain Markdown (see markdown.format: 'detect' in -// docusaurus.config.js, which decides this per file, by extension) — -// MDX and plain-Markdown pages go through different compilers, and a node -// valid for one throws in the other: -// - Plain Markdown (a synced .md guide): a raw mdast `html` node. That's -// exactly what mdast already uses for the hand-written raw HTML -// (`
`, `
`, ...) those pages rely on, and Docusaurus's -// plain-Markdown pipeline runs rehype-raw, which is what lets it (and -// this plugin's output) through to real HTML. -// - MDX (a hand-authored .mdx page): an `mdxJsxFlowElement` — the -// JSX-flavoured node MDX's own compiler expects in place of a `paragraph`. -// Docusaurus does NOT run rehype-raw for MDX, so a raw `html` node there -// fails the build with "Cannot handle unknown node `raw`" — an earlier -// version of this plugin emitted `html` unconditionally, which worked for -// every synced .md guide tested but would have broken the first .mdx page -// to actually use this pattern (caught in review before that happened). +// Emits an `mdxJsxFlowElement` node — the JSX-flavoured node MDX's own +// compiler expects in place of a `paragraph` — for BOTH plain Markdown and +// MDX pages, unconditionally. An earlier version emitted a raw mdast +// `html` node instead: that works for plain Markdown (Docusaurus runs +// rehype-raw there, the same thing that lets hand-written raw HTML like +// `
`/`
` through), but Docusaurus does NOT run rehype-raw for +// MDX, so an `.mdx` page hitting this pattern failed the build outright +// with "Cannot handle unknown node `raw`" (caught in review, see PR #14 — +// no `.mdx` page happened to use this pattern yet, so it shipped unnoticed +// until then). Switching to `mdxJsxFlowElement` for every file, not just +// `.mdx` ones, is the simpler fix: remark-rehype's own `passThrough` list +// for plain-Markdown files already includes `mdxJsxFlowElement`, so it +// flows through untouched into real HTML there too — verified against a +// throwaway `.md` and `.mdx` page, both compiling and rendering the +// identical iframe. One node shape, no format branching, and no manual +// `"`-escaping either (a JSX attribute is a plain AST string value, +// not text a parser has to read back out of markup). import {visit} from 'unist-util-visit'; const THUMBNAIL_RE = /^https:\/\/img\.youtube\.com\/vi\/([\w-]+)\//; @@ -43,23 +43,11 @@ const VIDEO_LINK_RE = /(?:youtu\.be\/|youtube\.com\/watch\?v=)([\w-]+)/; const IFRAME_ALLOW = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; -function buildHtmlNode(thumbnailId, title) { - // Raw HTML text, so the title has to be escaped by hand — this is the - // only one of the two node shapes where that's true (the MDX attribute - // below is a plain AST string value, not text mdast-util-mdx-jsx has to - // parse back out of markup, so it needs no escaping at all). - const escapedTitle = title.replace(/"/g, '"'); - return { - type: 'html', - value: `
`, - }; -} - function mdxAttr(name, value) { return {type: 'mdxJsxAttribute', name, value}; } -function buildMdxNode(thumbnailId, title) { +export function buildEmbedNode(thumbnailId, title) { return { type: 'mdxJsxFlowElement', name: 'div', @@ -83,27 +71,32 @@ function buildMdxNode(thumbnailId, title) { }; } -export default function remarkYoutubeEmbed() { - return (tree, file) => { - const isMdx = file?.extname === '.mdx'; +// Extracts { thumbnailId } if `node` is a paragraph matching the pattern, +// else undefined. Exported as a pure function so the matching logic can be +// unit tested without going through a full remark/unified pipeline. +export function matchYoutubeParagraph(node) { + if (!node || node.type !== 'paragraph' || node.children.length !== 1) return undefined; - visit(tree, 'paragraph', (node, index, parent) => { - if (!parent || index === null || node.children.length !== 1) return; + const link = node.children[0]; + if (link.type !== 'link' || link.children.length !== 1) return undefined; - const link = node.children[0]; - if (link.type !== 'link' || link.children.length !== 1) return; + const image = link.children[0]; + if (image.type !== 'image') return undefined; - const image = link.children[0]; - if (image.type !== 'image') return; + const thumbnailId = THUMBNAIL_RE.exec(image.url)?.[1]; + const linkId = VIDEO_LINK_RE.exec(link.url)?.[1]; + if (!thumbnailId || !linkId || thumbnailId !== linkId) return undefined; - const thumbnailId = THUMBNAIL_RE.exec(image.url)?.[1]; - const linkId = VIDEO_LINK_RE.exec(link.url)?.[1]; - if (!thumbnailId || !linkId || thumbnailId !== linkId) return; + return {thumbnailId, title: image.alt || 'Video'}; +} - const title = image.alt || 'Video'; - parent.children[index] = isMdx - ? buildMdxNode(thumbnailId, title) - : buildHtmlNode(thumbnailId, title); +export default function remarkYoutubeEmbed() { + return (tree) => { + visit(tree, 'paragraph', (node, index, parent) => { + if (!parent || index === null) return; + const match = matchYoutubeParagraph(node); + if (!match) return; + parent.children[index] = buildEmbedNode(match.thumbnailId, match.title); }); }; } diff --git a/test/list-submodule-tags.test.js b/test/list-submodule-tags.test.js new file mode 100644 index 0000000..e039315 --- /dev/null +++ b/test/list-submodule-tags.test.js @@ -0,0 +1,67 @@ +// Unit tests for scripts/list-submodule-tags.js's pure parsing/sorting +// logic — no network access, no real git invocation. See +// docs/contribute/versioning.mdx for how this fits into the versioning +// pipeline (picking Stable's tag, listing Previous versions). +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { parseLsRemote, compareSemver } = require('../scripts/list-submodule-tags'); + +test('parseLsRemote: annotated tag resolves to the peeled commit, plain line first', () => { + const output = [ + 'b551c0b1111111111111111111111111111111 refs/tags/v1.0.0', + 'ce638441111111111111111111111111111111 refs/tags/v1.0.0^{}', + ].join('\n'); + const tags = parseLsRemote(output); + assert.deepEqual(tags, [{ name: 'v1.0.0', commit: 'ce638441111111111111111111111111111111' }]); +}); + +test('parseLsRemote: annotated tag resolves to the peeled commit, peeled line first', () => { + const output = [ + 'ce638441111111111111111111111111111111 refs/tags/v1.0.0^{}', + 'b551c0b1111111111111111111111111111111 refs/tags/v1.0.0', + ].join('\n'); + const tags = parseLsRemote(output); + assert.deepEqual(tags, [{ name: 'v1.0.0', commit: 'ce638441111111111111111111111111111111' }]); +}); + +test('parseLsRemote: lightweight tag (no ^{} line) keeps its own SHA', () => { + const output = '58dcb371111111111111111111111111111111 refs/tags/v1.0.0'; + const tags = parseLsRemote(output); + assert.deepEqual(tags, [{ name: 'v1.0.0', commit: '58dcb371111111111111111111111111111111' }]); +}); + +test('parseLsRemote: non-semver tags and their ^{} lines are dropped', () => { + const output = [ + 'aaaaaaa1111111111111111111111111111111 refs/tags/latest', + 'bbbbbbb1111111111111111111111111111111 refs/tags/latest^{}', + 'ccccccc1111111111111111111111111111111 refs/tags/v1.0.0-rc1', + 'ddddddd1111111111111111111111111111111 refs/tags/foo', + 'eeeeeee1111111111111111111111111111111 refs/tags/v2.0.0', + ].join('\n'); + const tags = parseLsRemote(output); + assert.deepEqual(tags, [{ name: 'v2.0.0', commit: 'eeeeeee1111111111111111111111111111111' }]); +}); + +test('parseLsRemote: sorts newest first, numerically not lexically', () => { + const names = ['v1', 'v1.2', 'v1.9.0', 'v1.10.0', 'v2.0.0']; + const output = names + .map((name, i) => `${String(i).padStart(7, '0')}1111111111111111111111111111111 refs/tags/${name}`) + .join('\n'); + const tags = parseLsRemote(output); + assert.deepEqual( + tags.map((t) => t.name), + ['v2.0.0', 'v1.10.0', 'v1.9.0', 'v1.2', 'v1'] + ); +}); + +test('parseLsRemote: empty output gives []', () => { + assert.deepEqual(parseLsRemote(''), []); +}); + +test('compareSemver: orders descending, missing parts treated as 0', () => { + assert.ok(compareSemver('v2.0.0', 'v1.10.0') < 0); + assert.ok(compareSemver('v1.10.0', 'v1.9.0') < 0); + assert.ok(compareSemver('v1.9.0', 'v1.2') < 0); + assert.ok(compareSemver('v1.2', 'v1') < 0); + assert.equal(compareSemver('v1.0.0', 'v1.0.0'), 0); +}); diff --git a/test/prune.test.js b/test/prune.test.js new file mode 100644 index 0000000..8eac67b --- /dev/null +++ b/test/prune.test.js @@ -0,0 +1,111 @@ +// Unit tests for scripts/lib/prune.js — run against a throwaway temp +// directory (fs.mkdtempSync), no real submodule checkout or build needed. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const {pruneStale, pruneLegacyFolder, cleanupLegacyDestRoot} = require('../scripts/lib/prune'); + +function tempDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'prune-test-')); +} + +function write(root, relPath, content = 'x') { + const full = path.join(root, relPath); + fs.mkdirSync(path.dirname(full), {recursive: true}); + fs.writeFileSync(full, content); + return full; +} + +test('pruneStale: deletes synced files not in `written`, keeps the ones that are', () => { + const root = tempDir(); + const kept = write(root, 'a.md'); + const stale = write(root, 'b.md'); + const removed = pruneStale(root, new Set([kept])); + assert.deepEqual(removed, [stale]); + assert.ok(fs.existsSync(kept)); + assert.ok(!fs.existsSync(stale)); +}); + +test('pruneStale: never deletes index.* or README.*, even when not in `written`', () => { + const root = tempDir(); + const index = write(root, 'index.md'); + const indexMdx = write(root, 'sub/index.mdx'); + const readme = write(root, 'README.md'); + const removed = pruneStale(root, new Set()); + assert.deepEqual(removed, []); + assert.ok(fs.existsSync(index)); + assert.ok(fs.existsSync(indexMdx)); + assert.ok(fs.existsSync(readme)); +}); + +test('pruneStale: ignores extensions outside COPY_EXTS', () => { + const root = tempDir(); + const txt = write(root, 'notes.txt'); + const removed = pruneStale(root, new Set()); + assert.deepEqual(removed, []); + assert.ok(fs.existsSync(txt)); +}); + +test('pruneStale: removes directories it leaves empty', () => { + const root = tempDir(); + const stale = write(root, 'sub/deep/a.md'); + pruneStale(root, new Set()); + assert.ok(!fs.existsSync(path.join(root, 'sub'))); +}); + +test('pruneStale: a non-existent directory is a no-op', () => { + const root = tempDir(); + assert.deepEqual(pruneStale(path.join(root, 'missing'), new Set()), []); +}); + +test('pruneLegacyFolder: removes .md files, keeps .mdx, removes empty subdirs', () => { + const root = tempDir(); + const md = write(root, 'a.md'); + const indexMd = write(root, 'index.md'); // legacy folders get NO index/README exemption + const mdx = write(root, 'index.mdx'); + const nested = write(root, 'sub/b.md'); + pruneLegacyFolder(root); + assert.ok(!fs.existsSync(md)); + assert.ok(!fs.existsSync(indexMd)); + assert.ok(fs.existsSync(mdx)); + assert.ok(!fs.existsSync(path.join(root, 'sub'))); +}); + +test('cleanupLegacyDestRoot: no-op for a job without destRoot', () => { + const root = tempDir(); + write(root, 'docs/drivers/X/Y/_readme.md'); + const result = cleanupLegacyDestRoot(root, {to: 'X/Y/_readme.md'}); + assert.equal(result, undefined); + assert.ok(fs.existsSync(path.join(root, 'docs/drivers/X/Y/_readme.md'))); +}); + +test('cleanupLegacyDestRoot: removes a legacy folder job\'s old output, keeps a hand-authored index.mdx and sibling folders', () => { + const root = tempDir(); + write(root, 'docs/drivers/Adaptive grippers/Libraries/C++/API/a.md'); + write(root, 'docs/drivers/Adaptive grippers/Libraries/C++/API/index.md'); + const siblingIndex = write(root, 'docs/drivers/Adaptive grippers/Libraries/C++/docs/index.mdx'); + const siblingFolder = write(root, 'docs/drivers/Adaptive grippers/Other/GraspGen/index.mdx'); + + const result = cleanupLegacyDestRoot(root, {to: 'Adaptive grippers/Libraries/C++/API', destRoot: 'versioned-tools'}); + + assert.equal(result, path.join('docs', 'drivers', 'Adaptive grippers/Libraries/C++/API')); + assert.ok(!fs.existsSync(path.join(root, 'docs/drivers/Adaptive grippers/Libraries/C++/API'))); + assert.ok(fs.existsSync(siblingIndex)); + assert.ok(fs.existsSync(siblingFolder)); +}); + +test('cleanupLegacyDestRoot: removes a legacy file job\'s old output', () => { + const root = tempDir(); + const legacyReadme = write(root, 'docs/drivers/Tactile Sensor/Libraries/C++/_readme.md'); + const result = cleanupLegacyDestRoot(root, {to: 'Tactile Sensor/Libraries/C++/_readme.md', destRoot: 'versioned-tools'}); + assert.equal(result, path.join('docs', 'drivers', 'Tactile Sensor/Libraries/C++/_readme.md')); + assert.ok(!fs.existsSync(legacyReadme)); +}); + +test('cleanupLegacyDestRoot: no-op when nothing legacy exists', () => { + const root = tempDir(); + const result = cleanupLegacyDestRoot(root, {to: 'Tactile Sensor/Libraries/Python/_readme.md', destRoot: 'versioned-tools'}); + assert.equal(result, undefined); +}); diff --git a/test/site-nav-tree.test.mjs b/test/site-nav-tree.test.mjs new file mode 100644 index 0000000..e6783b4 --- /dev/null +++ b/test/site-nav-tree.test.mjs @@ -0,0 +1,103 @@ +// Unit tests for scripts/site-nav-tree.mjs. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import { + buildMainSidebar, + buildInstanceSidebar, + regenerateInstanceSidebar, + VERSIONED_TOOL_PATHS, +} from '../scripts/site-nav-tree.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +function collectVersionedLinks(items, found = []) { + for (const item of items) { + if (item.type === 'link') found.push(item); + if (item.type === 'category') { + assert.equal(item.link, undefined, `category "${item.label}" should not have a link in an instance sidebar`); + collectVersionedLinks(item.items, found); + } + } + return found; +} + +test('buildInstanceSidebar: the active tool node is exactly activeItem', () => { + const activeItem = {type: 'doc', id: 'index', label: 'C++'}; + const sidebar = buildInstanceSidebar('tactile-cpp', activeItem); + // Find it by walking: Tactile Sensor > Libraries > (activeItem) + const tactile = sidebar.find((n) => n.label === 'Tactile Sensor'); + const libraries = tactile.items.find((n) => n.label === 'Libraries'); + assert.deepEqual(libraries.items.find((n) => n.label === 'C++'), activeItem); +}); + +test('buildInstanceSidebar: every other versioned tool is a link to its encoded path', () => { + const sidebar = buildInstanceSidebar('tactile-cpp', {type: 'doc', id: 'index', label: 'C++'}); + const links = collectVersionedLinks(sidebar); + for (const [tool, urlPath] of Object.entries(VERSIONED_TOOL_PATHS)) { + if (tool === 'tactile-cpp') continue; + const expectedHref = encodeURI(urlPath); + const match = links.find((l) => l.href === expectedHref); + assert.ok(match, `expected a link with href ${expectedHref} for tool ${tool}`); + } +}); + +test('buildInstanceSidebar: href encoding — spaces become %20, + is left alone', () => { + const sidebar = buildInstanceSidebar('tactile-python', {type: 'doc', id: 'index', label: 'Python'}); + const links = collectVersionedLinks(sidebar); + const cpp = links.find((l) => l.label === 'C++' && l.href.includes('Tactile')); + assert.equal(cpp.href, '/docs/drivers/Tactile%20Sensor/Libraries/C++'); +}); + +test('buildInstanceSidebar: no category in an instance sidebar has its own link', () => { + const sidebar = buildInstanceSidebar('isaac-sim', {type: 'doc', id: 'index', label: 'Isaac Sim'}); + collectVersionedLinks(sidebar); // asserts internally on every category +}); + +test('buildMainSidebar: versioned-tool leaves are links, main-owned leaves are doc ids', () => { + const sidebar = buildMainSidebar(); + const adaptive = sidebar.find((n) => n.label === 'Adaptive grippers'); + const libraries = adaptive.items.find((n) => n.label === 'Libraries'); + const cpp = libraries.items.find((n) => n.label === 'C++'); + assert.equal(cpp.type, 'link'); + assert.equal(cpp.href, encodeURI(VERSIONED_TOOL_PATHS['adaptive-grippers-cpp'])); + // Python isn't versioned — plain doc id string, not a link object. + assert.ok(libraries.items.includes('drivers/Adaptive grippers/Libraries/Python/index')); +}); + +// The main regression test this file exists for: every already-cut +// version's frozen sidebar snapshot must already equal what +// regenerateInstanceSidebar produces from the CURRENT site-nav-tree.mjs. +// If this fails, someone changed site-nav-tree.mjs's SITE_TREE without +// re-running `node scripts/regenerate-versioned-sidebars.mjs` (also run +// automatically by `npm run generate` — see docs/contribute/versioning.mdx) +// — the same drift `ci.yml`'s "Verify generated content is committed" +// step would also catch after a real build, just faster and offline here. +test('drift guard: every committed versioned-sidebar snapshot matches what the live site nav tree would produce', () => { + const suffix = '_versioned_sidebars'; + const toolDirs = fs.readdirSync(ROOT, {withFileTypes: true}).filter((e) => e.isDirectory() && e.name.endsWith(suffix)); + assert.ok(toolDirs.length > 0, 'expected at least one _versioned_sidebars directory to exist'); + + for (const dirEntry of toolDirs) { + const tool = dirEntry.name.slice(0, -suffix.length); + const dir = path.join(ROOT, dirEntry.name); + for (const file of fs.readdirSync(dir)) { + if (!file.endsWith('-sidebars.json')) continue; + const filePath = path.join(dir, file); + const existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const [sidebarKey, existingItems] = Object.entries(existing)[0]; + + const regenerated = regenerateInstanceSidebar(tool, existingItems); + assert.notEqual(regenerated, undefined, `${path.relative(ROOT, filePath)}: could not locate '${tool}'s own content — site nav tree shape changed structurally`); + assert.deepEqual( + regenerated, + existingItems, + `${path.relative(ROOT, filePath)} is stale — run: node scripts/regenerate-versioned-sidebars.mjs` + ); + assert.equal(typeof sidebarKey, 'string'); + } + } +}); diff --git a/test/youtube-embed.test.mjs b/test/youtube-embed.test.mjs new file mode 100644 index 0000000..4a9b5ee --- /dev/null +++ b/test/youtube-embed.test.mjs @@ -0,0 +1,104 @@ +// Unit tests for src/remark/youtubeEmbed.mjs. +// +// matchYoutubeParagraph is tested directly against hand-built mdast nodes +// (no parser needed). The compile-level tests below reproduce the one +// Docusaurus-specific detail this plugin actually depends on — rehype-raw +// running for `format: 'md'` with `passThrough: ['mdxJsxFlowElement', ...]` +// (see @docusaurus/mdx-loader's processor.js) — using the real +// @mdx-js/mdx + rehype-raw packages already in node_modules (transitive +// deps of @docusaurus/mdx-loader), not a hand-rolled approximation. This +// is what catches a regression back to a plain `html` node, which would +// silently keep working for 'md' but break 'mdx' — exactly the bug this +// plugin shipped with once (see PR #14). +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {compile, run} from '@mdx-js/mdx'; +import * as runtime from 'react/jsx-runtime'; +import {renderToStaticMarkup} from 'react-dom/server'; +import rehypeRaw from 'rehype-raw'; +import remarkYoutubeEmbed, {matchYoutubeParagraph} from '../src/remark/youtubeEmbed.mjs'; + +const THUMB = 'https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg'; +const PATTERN = `[![Title](${THUMB})](https://youtu.be/dQw4w9WgXcQ)\n`; + +function paragraph(children) { + return {type: 'paragraph', children}; +} + +function link(url, children) { + return {type: 'link', url, children}; +} + +function image(url, alt) { + return {type: 'image', url, alt}; +} + +test('matchYoutubeParagraph: matches youtu.be links', () => { + const node = paragraph([link('https://youtu.be/dQw4w9WgXcQ', [image(THUMB, 'Title')])]); + assert.deepEqual(matchYoutubeParagraph(node), {thumbnailId: 'dQw4w9WgXcQ', title: 'Title'}); +}); + +test('matchYoutubeParagraph: matches youtube.com/watch?v= links', () => { + const node = paragraph([ + link('https://youtube.com/watch?v=dQw4w9WgXcQ&t=30s', [image(THUMB, 'Title')]), + ]); + assert.deepEqual(matchYoutubeParagraph(node), {thumbnailId: 'dQw4w9WgXcQ', title: 'Title'}); +}); + +test('matchYoutubeParagraph: falls back to "Video" with no alt text', () => { + const node = paragraph([link('https://youtu.be/dQw4w9WgXcQ', [image(THUMB, undefined)])]); + assert.equal(matchYoutubeParagraph(node).title, 'Video'); +}); + +test('matchYoutubeParagraph: no match when the thumbnail and link ids differ', () => { + const node = paragraph([link('https://youtu.be/otherId12345', [image(THUMB, 'Title')])]); + assert.equal(matchYoutubeParagraph(node), undefined); +}); + +test('matchYoutubeParagraph: no match when the image is not on img.youtube.com', () => { + const node = paragraph([ + link('https://youtu.be/dQw4w9WgXcQ', [image('https://example.com/dQw4w9WgXcQ.jpg', 'Title')]), + ]); + assert.equal(matchYoutubeParagraph(node), undefined); +}); + +test('matchYoutubeParagraph: no match when the paragraph has more than one child', () => { + const node = paragraph([link('https://youtu.be/dQw4w9WgXcQ', [image(THUMB, 'Title')]), {type: 'text', value: '!'}]); + assert.equal(matchYoutubeParagraph(node), undefined); +}); + +test('matchYoutubeParagraph: no match when the link has text next to the image', () => { + const node = paragraph([ + link('https://youtu.be/dQw4w9WgXcQ', [image(THUMB, 'Title'), {type: 'text', value: ' watch'}]), + ]); + assert.equal(matchYoutubeParagraph(node), undefined); +}); + +test('matchYoutubeParagraph: no match on a plain paragraph', () => { + assert.equal(matchYoutubeParagraph(paragraph([{type: 'text', value: 'hello'}])), undefined); +}); + +async function renderAsFormat(format) { + const rehypePlugins = + format === 'md' ? [[rehypeRaw, {passThrough: ['mdxJsxFlowElement', 'mdxJsxTextElement']}]] : []; + const compiled = await compile(PATTERN, { + format, + outputFormat: 'function-body', + remarkPlugins: [remarkYoutubeEmbed], + rehypePlugins, + }); + const {default: Content} = await run(compiled, runtime); + return renderToStaticMarkup(Content()); +} + +test('compiles and renders an iframe with format "md" (rehype-raw + passThrough, as Docusaurus configures it)', async () => { + const html = await renderAsFormat('md'); + assert.match(html, /class="video-wrapper"/); + assert.match(html, /]*src="https:\/\/www\.youtube\.com\/embed\/dQw4w9WgXcQ"/); +}); + +test('compiles and renders an iframe with format "mdx" — regression test for PR #14 (used to throw "Cannot handle unknown node `raw`")', async () => { + const html = await renderAsFormat('mdx'); + assert.match(html, /class="video-wrapper"/); + assert.match(html, /]*src="https:\/\/www\.youtube\.com\/embed\/dQw4w9WgXcQ"/); +}); From 7e3f1dcac913dc81bbaa7f7de4de1ac788b73b98 Mon Sep 17 00:00:00 2001 From: bcastets-robotiq Date: Mon, 28 Sep 2026 09:28:11 -0400 Subject: [PATCH 4/5] Address review: root URL serves Stable, API stability policy, typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocking items from review: - The instance root URL (a bare tool link, search result, or first visit) served Development (main) content, not a release — swapped the per-tool `versions` config so Stable owns the root path ('') and Development moves to '/next', banner-tagged 'unreleased' and marked noIndex so it's excluded from search/sitemap. Renamed "Latest" to "Development (main)" throughout (Docusaurus's own convention reserves "latest" for the newest *release*). - Required a follow-on fix: doxygen2docusaurus bakes every internal cross-reference as an absolute URL using the plugin's bare routeBasePath, with no idea Docusaurus versioning exists. Moving Development off the instance root broke every one of those links (every API page 404ing on itself) until `currentVersionPath` was added to thread that tool's `versions.current.path` through to the doxygen2docusaurus job. - Added docs/api-stability.mdx (only a tagged release is a compatibility commitment; main is experimental; the vMAJOR.MINOR.PATCH scheme), linked from the version banner, docs/intro.mdx, and a new "Use a released version" admonition on Development's own wrapper pages. Every doxygen2docusaurus-generated API page also gets a short experimental notice injected right after its frontmatter — "where users copy code from" per the review. - Rewrote the version banner to state the actual policy instead of just "unreleased". Other findings fixed: - Stable snapshots' README/source links pointed at `/tree/main/` and `/blob/main/` instead of the tag they're supposed to represent (the file at main can already differ from what shipped). Rewrote all 3 found. Documented the gap: no automation for this yet, has to be checked by hand on every cut (see versioning.mdx). - Wrong img alt text ("2F-85 gripper" on the C++/Python logos, copy- pasted boilerplate) across all 6 wrapper/snapshot pages, plus real typos ("developped", "the the", "Are below are") in the Tactile Sensor wrapper pages. - Dropped draft/documentation-versioning.md and its summary (.md + .pdf) — folded the still-relevant "why this design" rationale into versioning.mdx instead of linking out to a design-exploration doc not meant to live in this repo. Updated ~15 code-comment references across the codebase to point at versioning.mdx instead. - Reworded 3 passages in versioning.mdx that read as session narrative ("the user rejected it on sight", "this exact mistake has happened twice already") into neutral rules with rationale. Not done in this pass (flagged as improvements/discussion, not blocking, and each is its own real scope): - A `scripts/cut-version.js` to automate the manual checkout/sync/ version/restore sequence, and deriving the Stable label from metadata written at cut time instead of hand-typing it. The manual process is now documented accurately (including the main→tag rewrite step this pass added), but still fragile — worth a follow-up. - Whether Development's own API reference should be publicly indexed at all vs. kept to a preview/internal deploy — noIndex now keeps it out of search either way; left the publish-or-not question open per the review's own "for discussion" framing. Also merges main (PR #11, two Dependabot bumps) — this branch had fallen behind. Co-Authored-By: Claude Sonnet 5 --- .../version-stable/_readme.md | 2 +- .../version-stable/index.mdx | 4 +- docs/api-stability.mdx | 43 +++ docs/contribute/adding-a-tool.mdx | 2 +- docs/contribute/how-it-works.mdx | 6 +- docs/contribute/quick-reference.mdx | 2 +- docs/contribute/versioning.mdx | 154 +++++++--- docs/intro.mdx | 9 + docusaurus.config.js | 37 ++- draft/documentation-versioning-summary.md | 57 ---- draft/documentation-versioning-summary.pdf | Bin 4956 -> 0 bytes draft/documentation-versioning.md | 268 ------------------ scripts/external-jobs.js | 19 +- scripts/folder-sidebar.mjs | 2 +- scripts/generate-tools-table.js | 2 +- scripts/list-submodule-tags.js | 6 +- scripts/site-nav-tree.mjs | 4 +- scripts/sync-external-docs.js | 48 +++- sidebars.adaptive-grippers-cpp.js | 2 +- sidebars.isaac-sim.js | 2 +- sidebars.js | 9 +- sidebars.tactile-cpp.js | 2 +- sidebars.tactile-python.js | 2 +- src/theme/DocVersionBanner/index.jsx | 25 +- .../NavbarItem/ScopedDocsVersionDropdown.jsx | 4 +- .../version-previous-versions/index.mdx | 2 +- .../version-stable/_readme.md | 2 +- .../version-stable/index.mdx | 10 +- .../version-previous-versions/index.mdx | 2 +- .../version-stable/index.mdx | 10 +- .../Adaptive grippers/Libraries/C++/index.mdx | 9 +- .../Tactile Sensor/Libraries/C++/index.mdx | 15 +- .../Tactile Sensor/Libraries/Python/index.mdx | 15 +- 33 files changed, 338 insertions(+), 438 deletions(-) create mode 100644 docs/api-stability.mdx delete mode 100644 draft/documentation-versioning-summary.md delete mode 100644 draft/documentation-versioning-summary.pdf delete mode 100644 draft/documentation-versioning.md diff --git a/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md b/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md index 53bb8d4..f946884 100644 --- a/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md +++ b/adaptive-grippers-cpp_versioned_docs/version-stable/_readme.md @@ -82,7 +82,7 @@ Visual Studio build would have to compile libserialport itself. A complete example — waiting for motion to settle, reading the position back, injecting a log sink — is built as described in the Building section and can be found here: -[`sdk_cpp/examples/move_gripper.cpp`](https://github.com/robotiq/grippers/blob/main/sdk_cpp/examples/move_gripper.cpp) +[`sdk_cpp/examples/move_gripper.cpp`](https://github.com/robotiq/grippers/blob/v1.0.0/sdk_cpp/examples/move_gripper.cpp) Run it by executing: ```sh diff --git a/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx b/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx index c30f48a..929d4eb 100644 --- a/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx +++ b/adaptive-grippers-cpp_versioned_docs/version-stable/index.mdx @@ -4,7 +4,7 @@ sidebar_label: C++ ---
- 2F-85 gripper + C++ logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) @@ -17,5 +17,5 @@ import Readme from './_readme.md'; ## Source Code - GitHub Repository + GitHub Repository diff --git a/docs/api-stability.mdx b/docs/api-stability.mdx new file mode 100644 index 0000000..946d7f1 --- /dev/null +++ b/docs/api-stability.mdx @@ -0,0 +1,43 @@ +--- +title: API Stability Policy +sidebar_label: API stability policy +--- + +# API stability policy + +This page applies to every Robotiq-maintained software tool on this site +that has its own **Stable / Development (main) / Previous versions** +switcher (see +[Per-tool documentation versioning](/docs/contribute/versioning) for how +that switcher works). + +## Only a tagged release is a commitment + +**Stable** always tracks a tool's newest tagged release. That's the only +version this site makes any compatibility commitment about — once a tag +is cut, the API it documents doesn't change under you. + +**Development (main)** documents whatever is currently on the source +repository's default branch, which can include work in progress: renamed +methods, changed signatures, new required parameters, or symbols removed +entirely, all without notice. Nothing on a Development page is a +commitment, even if it looks finished. If you're integrating against one +of these tools, build against **Stable**, not Development. + +## Versioning scheme + +Each tool tags its own releases independently — there's no single +site-wide version number, since this site aggregates several +independently-released repositories. A tool's tags follow `vMAJOR.MINOR.PATCH` +(semantic versioning): a `MAJOR` bump can break compatibility, `MINOR` +adds functionality without breaking existing callers, and `PATCH` is a +fix with no API change. + +## Previous versions + +Older tagged releases aren't archived on this site — only **Stable** and +**Development (main)** are real, rebuilt versions. A tool's **Previous +versions** page +lists its older tags with a link to that tag's own source, so you can +still read matching documentation (and check out matching code), just not +in place on this site. diff --git a/docs/contribute/adding-a-tool.mdx b/docs/contribute/adding-a-tool.mdx index 8a96a87..c680bfe 100644 --- a/docs/contribute/adding-a-tool.mdx +++ b/docs/contribute/adding-a-tool.mdx @@ -12,7 +12,7 @@ like every non-versioned tool. **If the new tool is Robotiq-maintained and submodule-synced** (as opposed to a third-party or not-yet-available tool), it should usually also get its -own **Latest / Stable / Previous versions** switcher, matching every other +own **Stable / Development (main) / Previous versions** switcher, matching every other Robotiq-maintained tool on this site — see [Per-tool documentation versioning](./versioning) once the steps below are done. That changes *where* the tool's files live (`versioned-tools/` diff --git a/docs/contribute/how-it-works.mdx b/docs/contribute/how-it-works.mdx index 65a6be8..49fdce4 100644 --- a/docs/contribute/how-it-works.mdx +++ b/docs/contribute/how-it-works.mdx @@ -70,7 +70,7 @@ robotiq.github.io/ │ └── Other/GraspGen/ … ← "Other" category ├── versioned-tools/ ← content for VERSIONED tools (own │ │ Docusaurus plugin instance each, -│ │ Latest/Stable/Previous versions — +│ │ Stable/Development/Previous versions — │ │ see contribute/versioning.mdx), │ │ mirrors docs/drivers/'s own shape │ │ one level in from @@ -88,7 +88,7 @@ robotiq.github.io/ │ │ └── _readme.md │ └── Adaptive grippers/ │ ├── Libraries/C++/ … ← same guides+API shape as above -│ └── Simulation/Isaac Sim/ … ← single page, Latest only so far +│ └── Simulation/Isaac Sim/ … ← single page, Development (main) only so far ├── draft/ ← content with no nav link yet (never │ built into a page — outside docs/) ├── scripts/ @@ -105,7 +105,7 @@ wrapper `.mdx` but not standalone pages themselves. **A tool listed above as "VERSIONED" lives under `versioned-tools/`, not `docs/`, and has its own Docusaurus plugin instance with its own -Latest/Stable/Previous versions switcher** — see +Stable/Development/Previous versions switcher** — see [Per-tool documentation versioning](./versioning) for the whole mechanism, why it needs a separate top-level folder rather than living inside `docs/`, and how to add it to a new tool. Everything else on this page diff --git a/docs/contribute/quick-reference.mdx b/docs/contribute/quick-reference.mdx index fd7b9d4..bf493ab 100644 --- a/docs/contribute/quick-reference.mdx +++ b/docs/contribute/quick-reference.mdx @@ -11,7 +11,7 @@ displayed_sidebar: contributeSidebar | Submodules (pinned repo imports) | `external//` | | Sync + generation config | `scripts/external-jobs.js` → `JOBS`, authored via `submoduleJobs(submodule, { repoUrl, branch }, [...])` per submodule | | C++ API sidebar (auto-built from Doxygen groups) | `scripts/generated/doxygen-sidebar--.json`, written by `sync-external-docs.js` and read back by `sidebars.js`/`sidebars..js`'s `loadDoxygenSidebarItems` — mirrors the `\defgroup`/`\ingroup` hierarchy with no manual sidebar entries; see [The sidebar mirrors the Doxygen group hierarchy automatically](./api-reference-cpp#the-sidebar-mirrors-the-doxygen-group-hierarchy-automatically) | -| Per-tool Latest/Stable/Previous versions | one `@docusaurus/plugin-content-docs` instance per versioned tool in `docusaurus.config.js`'s `plugins` array, content under `versioned-tools////` (not `docs/`) — see [Per-tool documentation versioning](./versioning) | +| Per-tool Stable/Development/Previous versions | one `@docusaurus/plugin-content-docs` instance per versioned tool in `docusaurus.config.js`'s `plugins` array, content under `versioned-tools////` (not `docs/`) — see [Per-tool documentation versioning](./versioning) | | Full site nav tree, shared by the main sidebar and every versioned tool's own sidebar | `scripts/site-nav-tree.mjs` — see [Per-tool documentation versioning](./versioning) | | One-command preview of pushed WIP submodule commits | `npm run preview` in a **second terminal**, run **before** starting terminal 1 with `SKIP_SUBMODULE_RESET=1 npm start` (plain `npm start` resets the submodule via its own `prestart` and undoes the preview) — **not** `npm start preview`, which mis-parses as a `docusaurus start` site-directory argument — see [Previewing local edits to a submodule](./#previewing-local-edits-to-a-submodule) | | Exclude README content from the synced page | `` ... `` in the tool repo's README | diff --git a/docs/contribute/versioning.mdx b/docs/contribute/versioning.mdx index 950cef7..1d42c5d 100644 --- a/docs/contribute/versioning.mdx +++ b/docs/contribute/versioning.mdx @@ -1,29 +1,92 @@ --- title: Per-tool Documentation Versioning -sidebar_label: Versioning (Latest/Stable) +sidebar_label: Versioning (Stable/Development) displayed_sidebar: contributeSidebar --- # Per-tool documentation versioning Every Robotiq-maintained, submodule-synced tool gets its own independent -**Latest / Stable / Previous versions** switcher (the dropdown next to the -navbar on that tool's own pages) — full design rationale in -[`draft/documentation-versioning.md`](https://github.com/robotiq/robotiq.github.io/blob/main/draft/documentation-versioning.md). -A tool that isn't submodule-synced (a hand-authored placeholder like -PyBullet/MuJoCo/GraspGen) has nothing to version against and stays on the -plain, single-version pattern described in -[Adding a new software tool](./adding-a-tool) — this page only applies once -a tool has a real submodule and real tags. - -- **Latest** tracks the submodule's default branch — the same content - `sync-external-docs.js` always produced, just now living at its own - version path instead of being the only version that exists. -- **Stable** tracks the submodule's newest tag. -- **Previous versions** isn't a real build. It's one static "signpost" page - per tool, linking out to each *older* tag's own source on GitHub — this - site doesn't archive full docs for every past release, just the two that - matter (current dev, current release). +**Stable / Development (main) / Previous versions** switcher (the dropdown +next to the navbar on that tool's own pages). A tool that isn't +submodule-synced (a hand-authored placeholder like PyBullet/MuJoCo/GraspGen) +has nothing to version against and stays on the plain, single-version +pattern described in [Adding a new software tool](./adding-a-tool) — this +page only applies once a tool has a real submodule and real tags. See the +[API stability policy](/docs/api-stability) for what this means for a +reader deciding which version to build against. + +## Why a 3-way switcher, not full per-tag archiving + +This site aggregates N independently-released repos (currently 3 +submodules) — there's no single "site version" a `grippers` release and a +`tactile_sensors` release both correspond to, so full per-tag versioning +(one archived build per release, per tool) would multiply without bound as +tools and tags are added. The chosen shape stays bounded at **exactly two +real builds per tool, forever**, plus one page that costs nothing per tag: + +- **Stable** — always the newest tag. A real, released state, rebuilt only + when that tag moves. Owns the tool's own root URL (`path: ''`) — the + default a bare link, a search result, or a first-time visitor lands on, + since that's the version this site actually makes a compatibility + commitment about. +- **Development (main)** — always `main`, unchanged in content from + before this feature existed, just relocated off the root URL to `/next` + (`path: 'next'`) and tagged `banner: 'unreleased'` + `noIndex: true` + (kept out of search/sitemap). May include unreleased, uncommitted API + changes at any time. +- **Previous versions** — not a real build. One static signpost page per + tool, linking each older tag straight to its own source on GitHub, so + readers checking an older release still get to matching docs without + this site having to host or rebuild them. + +An older-than-Stable reader doesn't get pixel-matching docs on this site — +they're pointed at the source repo's own tag instead, the same place `git +tag`/GitHub Releases already is the source of truth. That's a deliberate +trade-off: it's what keeps "Previous versions" free of the combinatorial +cost full archiving would add, while still answering the common case (a +reader on a released version, not chasing `main`) directly. Other shapes +considered and set aside: whole-site periodic snapshots (imprecise — one +tool's release date doesn't mean anything for the others) and pushing +versioning out to each tool repo entirely (loses the unified +in-place-on-this-site reading experience for no real savings, since this +site already carries substantial aggregation-pipeline complexity anyway). + +## Which version the root URL serves + +Configured per tool in `docusaurus.config.js`'s `versions`: + +```js +versions: { + current: { label: 'Development (main)', path: 'next', banner: 'unreleased', noIndex: true }, + stable: { label: 'Stable (vX.Y.Z)', path: '' }, + 'previous-versions': { label: 'Previous versions', path: 'previous-versions' }, +}, +``` + +`stable` takes the empty `path: ''` — Docusaurus serves whichever version +has that path at the instance's own root URL (e.g. +`/docs/drivers/Tactile Sensor/Libraries/C++`), with no version segment in +the URL at all. `current` moves to `path: 'next'` instead (matching +Docusaurus's own convention for unreleased docs elsewhere), and carries +`banner: 'unreleased'` (renders the `src/theme/DocVersionBanner/index.jsx` +warning — see [API Stability Policy](/docs/api-stability)) and +`noIndex: true` (a `` tag, so search +engines and the sitemap never send a reader to unreleased docs by +default). + +**A generated API reference needs one more piece wired up for this to +work.** doxygen2docusaurus bakes every internal cross-reference +(``) as an ABSOLUTE URL directly into the generated +HTML — it has no idea Docusaurus versioning exists, so it only ever uses +the plugin instance's bare `routeBasePath` as the prefix, never that +version's own path segment. Since this pipeline always writes the +*current* version's content, moving `current` off the instance root means +every one of those backlinks needs `/next` appended, or they 404. Set +`currentVersionPath` on that tool's `doxygen2docusaurus` job in +`scripts/external-jobs.js`, matching `versions.current.path` exactly — +see the `2f85_cpp` job and the comment on `currentRoutePrefix` in +`sync-external-docs.js` for the mechanics. ## Why each versioned tool is a separate Docusaurus plugin instance @@ -57,11 +120,12 @@ need to be the instance-relative tail, not the site-relative `to`). A plugin instance can only build sidebar items from doc ids it owns — everything else has to be a plain link. Because of that, each versioned tool's `sidebars..js` (e.g. `sidebars.tactile-cpp.js`, -`sidebars.adaptive-grippers-cpp.js`) does **not** just list that tool's own -page(s) — an earlier version of this did exactly that, and the user -rejected it on sight: navigating into a versioned tool replaced the entire -left sidebar with just that tool's tiny one, losing every other -product/tool and feeling like "a disconnected page." +`sidebars.adaptive-grippers-cpp.js`) must **not** just list that tool's own +page(s). Listing only the tool's own pages replaces the entire left +sidebar the moment a reader navigates into a versioned tool — every other +product/tool disappears, and the page reads as a disconnected site rather +than part of this one. The whole-tree navigation is a hard requirement for +any new versioned instance, not a nice-to-have. The fix, and the pattern any new versioned tool must follow: `scripts/site-nav-tree.mjs` is the single source of truth for the whole @@ -160,7 +224,7 @@ cd ../.. SKIP_SUBMODULE_RESET=1 node scripts/sync-external-docs.js npm run docusaurus -- docs:version: stable cd external/ && git checkout main && cd ../.. -SKIP_SUBMODULE_RESET=1 node scripts/sync-external-docs.js # restore Latest's own content +SKIP_SUBMODULE_RESET=1 node scripts/sync-external-docs.js # restore Development (main)'s own content ``` `SKIP_SUBMODULE_RESET=1` is required — `sync-external-docs.js` runs `git @@ -168,6 +232,19 @@ submodule update --init --force` on every normal invocation, which would silently revert the manual `git checkout ` right back to the pinned commit before the sync even ran. +**Rewrite any `main`-branch source links to the tag, before cutting.** +Content synced from the submodule (its README, guides) routinely links +back to its own source on GitHub — `tree/main/...`, `blob/main/...` — which +is correct for Development but wrong once that same text is frozen into a +Stable snapshot: the file at `main` can already differ from what shipped +in the tag Stable is supposed to represent. Rewrite every such link to +`tree//...` / `blob//...` in the checked-out content *before* +running `docs:version:`, and do the same for any hand-authored "Source +Code" button/CTA on the tool's own wrapper page +(`https://github.com//` → `https://github.com///tree/`). +There's no automation for this yet — grep the synced content for +`/main/` under `github.com/robotiq/` before cutting, and check by hand. + **Watch for stale-content contamination.** `sync-external-docs.js` never deletes a previously-written folder (e.g. a tool's `docs/` guides or generated `API/`) just because the currently-checked-out tag's job skips @@ -179,10 +256,10 @@ snapshot too. Before cutting a version that should be sparse (an early tag that predates a guide folder, or a `previous-versions` signpost that should hold only its own hand-written `index.mdx`), move anything the current tag's own sync wouldn't produce out of the way first, cut, then -restore it afterward for `Latest`'s own benefit. Verify with a plain +restore it afterward for Development's own benefit. Verify with a plain `find _versioned_docs/version- -type f` before trusting the -cut — this exact mistake has happened twice already (a -`docs`/`API` tree leaking into what should have been a two-file signpost). +cut — skipping this step is an easy way to leak a stale `docs`/`API` tree +into what should have been a two-file signpost. **Writing the `Previous versions` signpost.** It's a hand-authored `index.mdx`, temporarily written over the live wrapper page (back it up @@ -197,9 +274,9 @@ sidebar_label: C++ **Stable** currently tracks ``'s newest release, **vX.Y.Z**. -We only host **Latest** and **Stable** here — older releases aren't -archived on this site. Browse their own tag in the source repository -instead: +We only host **Development (main)** and **Stable** here — older releases +aren't archived on this site. Browse their own tag in the source +repository instead: - **vA.B.C** — [browse source](https://github.com/robotiq//tree/vA.B.C) ``` @@ -207,14 +284,13 @@ instead: **List only tags OLDER than the one Stable tracks — never re-list Stable's own tag.** A tool with only one tag total has *nothing* to list here yet; say so explicitly ("there are no older releases archived here yet") rather -than reusing another tool's list shape unchanged. This was a real bug, -found and fixed after shipping: `adaptive-grippers-cpp`'s `2f85_cpp` only -has one tag (the one Stable already tracks), but its signpost was authored -by copying `tactile_sensors`' (which genuinely has an older tag distinct -from its Stable) without checking whether the same shape applied — -"exclude whatever Stable tracks" is the actual rule, not "list every tag -that isn't literally the newest," and those only coincide once a tool has -2+ tags. +than reusing another tool's list shape unchanged. "Exclude whatever Stable +tracks" is the actual rule, not "list every tag that isn't literally the +newest" — those only coincide once a tool has 2+ tags, so copying a +multi-tag tool's signpost as a template for a single-tag one silently +re-lists Stable's own tag as if it were archived separately. Check each +tool's actual tag count before authoring its signpost; don't assume the +same shape applies. **Make the Stable tag visible.** Nothing on a Stable page itself ever prints which tag it is — without a visible marker, the only tag a visitor @@ -224,7 +300,7 @@ silently *is* the newest one. Label it in `docusaurus.config.js`'s `versions` config: ```js -stable: { label: 'Stable (v2.0.0)', path: 'stable' }, +stable: { label: 'Stable (v2.0.0)', path: '' }, ``` Update this by hand every time Stable is re-cut to a newer tag. diff --git a/docs/intro.mdx b/docs/intro.mdx index cd5e990..f9eaaaa 100644 --- a/docs/intro.mdx +++ b/docs/intro.mdx @@ -19,6 +19,15 @@ For details about `Robotiq` product hardware, please refer to the corresponding Hardware manual available on [Robotiq support website](https://robotiq.com/support). ::: +Some `Robotiq`-maintained tools carry their own **Stable / Development +(main) / Previous versions** switcher, next to the navbar on that tool's own +pages. **Stable** is the version you want if you're integrating against +one of these tools — it always tracks that tool's newest tagged release, +and is the only version this site makes any compatibility commitment +about. **Development (main)** documents in-progress work and can change +without notice. See the [API stability policy](/docs/api-stability) for +the full details. + ## Software tools Here below is a summary about available tools. diff --git a/docusaurus.config.js b/docusaurus.config.js index 2ecd70a..b40db61 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -101,8 +101,8 @@ theme: { ], ], - // Per-tool documentation versioning (Latest / Stable / Previous versions) - // — see draft/documentation-versioning.md. Only Robotiq-maintained, + // Per-tool documentation versioning (Stable / Development (main) / + // Previous versions) — see docs/contribute/versioning.mdx. Only Robotiq-maintained, // submodule-synced tools get their own instance like this (one per // instance below); product/ROS/third-party pages stay on the single // instance above, which has nothing to version against (no submodule, @@ -130,8 +130,15 @@ theme: { rehypePlugins: [rehypeExternalLinksNewTab], includeCurrentVersion: true, lastVersion: 'stable', + // Stable owns the root path (''), not `current` — a first-time + // visitor (or an external link, or a search result) should land on + // a released version, not on whatever `main` happens to be at that + // moment. `current` moves to `next` instead, banner-tagged + // 'unreleased' and excluded from search/sitemap (`noIndex`) so it's + // never what search sends someone to. See "Which version the root + // URL serves" in docs/contribute/versioning.mdx. versions: { - current: { label: 'Latest', path: '' }, + current: { label: 'Development (main)', path: 'next', banner: 'unreleased', noIndex: true }, // Labeled with its tag: without this, the tag Stable actually // tracks isn't visible anywhere on the site, which reads as // "only 1 of tactile_sensors' 2 releases is on this site" even @@ -139,7 +146,7 @@ theme: { // versioned-tools' version-previous-versions/index.mdx for the // matching explanation. Update this by hand whenever Stable is // re-cut to a newer tag (see scripts/list-submodule-tags.js). - stable: { label: 'Stable (v2.0.0)', path: 'stable' }, + stable: { label: 'Stable (v2.0.0)', path: '' }, 'previous-versions': { label: 'Previous versions', path: 'previous-versions' }, }, }), @@ -156,10 +163,10 @@ theme: { rehypePlugins: [rehypeExternalLinksNewTab], includeCurrentVersion: true, lastVersion: 'stable', + // See the matching comment on 'tactile-python' above. versions: { - current: { label: 'Latest', path: '' }, - // See the matching comment on 'tactile-python' above. - stable: { label: 'Stable (v2.0.0)', path: 'stable' }, + current: { label: 'Development (main)', path: 'next', banner: 'unreleased', noIndex: true }, + stable: { label: 'Stable (v2.0.0)', path: '' }, 'previous-versions': { label: 'Previous versions', path: 'previous-versions' }, }, }), @@ -176,7 +183,9 @@ theme: { rehypePlugins: [rehypeExternalLinksNewTab], // No lastVersion/versions config yet — isaacsim_assets has no tags, // so there's nothing to cut a 'stable'/'previous-versions' version - // from. Only 'Latest' exists for now. Deliberately no matching + // from. Only the current (Development/main) content exists for now, + // still at this instance's own root path (no 'next' split needed + // until there's an actual Stable to make room for). Deliberately no matching // navbar item below either: with only one version, Docusaurus // renders it as a plain "Current" button rather than hiding it — // clutter with no payoff until this submodule gets its first real @@ -197,12 +206,12 @@ theme: { rehypePlugins: [rehypeExternalLinksNewTab], includeCurrentVersion: true, lastVersion: 'stable', + // See the matching comment on 'tactile-python' above. 2f85_cpp + // only has one tag so far (v1.0.0) — Previous versions has + // nothing older to list yet, see that version's own index.mdx. versions: { - current: { label: 'Latest', path: '' }, - // See the matching comment on 'tactile-python' above. 2f85_cpp - // only has one tag so far (v1.0.0) — Previous versions has - // nothing older to list yet, see that version's own index.mdx. - stable: { label: 'Stable (v1.0.0)', path: 'stable' }, + current: { label: 'Development (main)', path: 'next', banner: 'unreleased', noIndex: true }, + stable: { label: 'Stable (v1.0.0)', path: '' }, 'previous-versions': { label: 'Previous versions', path: 'previous-versions' }, }, }), @@ -244,7 +253,7 @@ theme: { // The stock 'docsVersionDropdown' navbar item always renders, // site-wide — outside its own instance it just falls back to a // link instead of disappearing, which isn't the scoping this - // needs (see "Scope" in draft/documentation-versioning.md: each + // needs (see "Scope" in docs/contribute/versioning.mdx: each // of these must only appear on its own instance's own pages). // src/theme/NavbarItem/ScopedDocsVersionDropdown.jsx wraps it // with that visibility check; ComponentTypes.js registers it diff --git a/draft/documentation-versioning-summary.md b/draft/documentation-versioning-summary.md deleted file mode 100644 index 7731f4a..0000000 --- a/draft/documentation-versioning-summary.md +++ /dev/null @@ -1,57 +0,0 @@ -# Documentation versioning — the plan - -*Short version. Full reasoning and alternatives: `documentation-versioning.md`.* - -## The problem - -This site always shows the latest (`main`) state of each submodule. A -customer on an older tagged release can't tell the docs have moved on, and -has no way to see docs matching what they actually have. - -## The plan: a 3-way version switcher, only where it matters - -A dropdown with three entries, shown **only on pages that come from a -Robotiq-maintained submodule** (a tool's own page, its API reference, its -synced guides): - -| Entry | What it shows | -|---|---| -| **Latest** | Today's behavior — every submodule at `main`. May include unreleased changes. | -| **Stable** | Every submodule at its own newest tag. A real, released state. | -| **Previous versions** | Not real docs — one static page: "for an older release, check that product's own tags," with a link per submodule. | - -**Not shown on:** product/ROS/Simulation/Other landing pages (they mix -several tools), third-party tool pages (pyRobotiqGripper, community ROS -packages, PyBullet, MuJoCo, GraspGen, ...), or `docs/contribute/`. None of -those are backed by a submodule we tag-track, so there's nothing to -version. - -## Why this and not full per-tag versioning - -- **Bounded:** always at most 2 real builds (Latest, Stable) per tool, - never one per tag. Adding submodules or tags never multiplies the work. -- **"Previous versions" costs nothing:** it's one static page, not a - rebuild — we don't host every old release, we just point at it. -- Covers the real, common case (matching a *released* version) without - building infrastructure for the rare one (matching an *old* release - exactly). - -## What's new to build - -1. A way to track each submodule's *newest tag*, separately from the - existing `main`-tracking (Dependabot only does branches, not tags). -2. A small, versioned Docusaurus docs instance per Robotiq-maintained - tool — this is also what scopes the dropdown to only those pages, for - free. -3. One hand-authored signpost page for "Previous versions," listing each - submodule with a link to its tags/releases. -4. A small "which version am I looking at" stamp on Latest and Stable - pages. - -## Open before building - -- How exactly do we track "latest tag" per submodule (new script, mirrors - `check-submodule-pins.js`)? -- Do all submodules tag consistently enough (`vX.Y.Z`) to sort on? -- New-tool checklist (`adding-a-tool.mdx`) needs a step for the new - per-tool versioned instance. diff --git a/draft/documentation-versioning-summary.pdf b/draft/documentation-versioning-summary.pdf deleted file mode 100644 index 609b3375d4c152a587978aa7ad47079f73b7d1d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4956 zcmdT|$-=5ilHUJMfjEJR41(e;4mjYz;D7@vvp4_(R^8i+eu3^?@B3LB&;3u`zs{+y zI=yTbk{LusWJG)sW@MW0vj|=!Khpp8pa1dSfN1(&5KUDL7@DpbsF7&^F$Z9H`s-o3 z5de9<>4x4b&`tOQ2Qa|Xt{;uY2g>3A5l=+`WeK44IVu6<=ZFNT&k+Tb-=mLK>%(-7 z_wx6;?_#=YXtG|c2pyUCznGDus{lH1b1IjVIQY8QdKzTKIV-tVlF$SP${sGVs_y){3nm)hqfu~Dm6iYzgs79COZ!yxw z57joipYPu1dkiQsZen3x6lXy!7+nUc`%UDV{sDYP7#}bGf-pK}{qB`NP54ZR{~sn) zznJj0Zsr*=LRXy)>9+(?%7}bfWKq(-4V1B4*@SX^iAEd9MkwFf-`^r zs}~p{0L6UqPXCpt7y`yik23-Kv(~+_Fm<#*4Soj@;P9o!y;ZzJ9T%6YsmJB-0R6au zr4!xXoBynk-|PYYVg_Ms*SlsN2#f@xiwXF5)s6QU!^rpJvWl|mJ^YHCZyx>bu{d92 z*v4gsb7I@}z&kTU;0-SEjrc#j{w)$;eg3tQvd%q<&5WVWNWJlxrut@FMc*ay9(((9 z5{sIet{q(ff+!PjQDGFvt6{Z*e%F35s^ zwVe`m?zkciZuXfLF-hO;m+PF0boRjR(}@d|m$XgMU>KyCMKk?Y&hj#om(kP?IygO8$uu!@w`w^2WUBKr#m9wr)YF&A7 z5VjwPNTLeo?L(UEbkJ#gQn@$}UYlIIB5a<@RVkHJkSSlIv$t(&G!9tl+^>TMBHkr+q(_5Uw`d8$Dyb5fl8fYFZNh3&?m;m1$S$U); zA}424IYqFQ67B9 zDMXz++{$I?B|F%k26ESaBn(I&GyOGPb`n=|O^B2%)Z4|5Fv^_Cc5B3mDyNU;Nz~a+ zJ?9Z@hWeoBy4re`PQp})YKh<|XL4aH32_oZ)hCVi43h-$Kx9?2)aig8oS#&-OGBc! zoQHzW*=jG++|k+5KKl&%G+wZ^dyYZaA-sB(a1UBm9`*9xip1xPn<~A5lr0`MYsj+T z>jEtHg$w;2mHb`D_J2?xY<6PG|Da;far#N*7(Rpk% zBJ1vQp3EpWYlzoazdJeP>CVl;8i#^1THGw#BMnd3@4}OtcooN9Da2EQct49GD~TvMS%2lWhmG-RE>=Ki%g;pkPsLEU%LWC@%!NGLWRin8T2K zbnGo@29@_g2gW}AWE-0xnA#l94#Q?fsp9S=k5 zag7Ke>oEREeU#?=E?85|Ql&RyZZGuSV^J}IuWS(~uUR~W=3!FgZcN^FN43*bG3Cc$ zRfiMC7`Z_6L3VsJ{DqvyTWtx%1;`{DB+s>!o0E1ar&k-m`gtPo_Pv8i!>qrp=KMQ* zkT%PcggwXUz_>0Jk)$J!$m1GQGS`>aSl8=4W&=tvqgq_mo8t5;3~E9$gO$zAq4fk4 zf$vhmF}e3U&)JIYg6%G{sJN#QxX`+lq)-bI9?DOaq_^-+`m0sH(niw4_t3a%u>NYl zCc)%rJRRM9DQe)o?1&8-96DGeQEEbRD`7m@!=2MpN(EP^D{@QonV%}HapM%6wnRJG zXTMf=MDmNgHE?Vt(VZ9o-ysaMe3H+?r@;j^vCPn?JbnpoQZ_Q3%RpzfnwwN+f z_vIKoDXBL67~?BF=MP-%(A(luVgBqy>GW|O1uwbJt(vP__ELHFhc&a)t>h53w@mg` zgIV_P1vl5rfo>+$`*@O_DE(+pQ8gC)qE$JE*maS~Jq(Jz_}O0rRp- zwB$){H4(I6$sG3R%^FM=xYG%1Lepzim>6wb*3Oi()+^$61M+=LAk!(1-k2iD-3sa9 zCUdGfy>@|Cx!WS_M$f`4zZ9m&IeKsDFW;;PUbcQ(*@UEWe%8f11F;Atx0|Q1^9Ct- z{8AGS{tEFVT1oVi4_zI-%4MdNGtzWN?kX!+ou&DG{x-GN!@j^=j_}o}FXi-gbw>-; z^Fb3Vn+t1NE{_}nSeTTE7FKX(`(u*`4;Wi(R}-_=1%5JjUYGWQd*@x z+YNhZ8X|AFbr!vCp~lt*hxU2dPNpZzO^`F-M*VycL~Pj}oh#$(Q*o$OG1^aV8b9l9 zvK-fIJ_xlim#W!$4x)^3wj9q-pmaTBVEW2sM$Iy@+=+=&sjt;@b)PLO1q>=%fMTPXCT@Zp-=?x3s4^Hr?Oc&=J( zZn%5d0~hA$mS*{#Mz=_)o64pVM|kQ#n3e9F<0Doz3v&m%2d(aBKY+N7kyD=;?vlL! zdn;}@KhHOxttbAqA^bm{n5fbZ2PU<4LgOfArx&e%?t3o%uP~xXpI6&+1*ULvcm4r$Mr3G#A8w;@)J=ilal)dtNtK;Qc!Gml2p~v%@u*^5OvBUR}X+fU9B-|TG z3O4rzRAP$faa!n0%v?EUAQ}qG_q^YbXV;N=rLmcB7jw0X21;FeSN5T~tH`ArDvu{J zawhugCS^Awz0+Gkl|2=Z1fFVkxJey#<#<*Yyxh!|=8~+QT4^ZxYx!pia9)$imXvQgQnq!0ordv_3EZCqJ;iJwTa? z&s~58{Eb$QTdVg2xPPPJrTA$71C1!fjoJ^i67}<1vK)8nKk!kNpZUsEd|UAYA5H$G z1INgp_;CE4NPoW;C-I-xl5~7M@%MZs`l}sLpgMOm_;#f`, while the rest of the site (product pages, -guides, other languages) stays on the single unversioned instance it uses -today, since those don't move fast enough to justify this. - -Mechanically, per tool: - -1. A new CI trigger that watches for a **tag**, not just the default - branch — `dependabot.yml`/`check-submodule-pins.js` only ever look at - the default branch today, so this is new: either a `repository_dispatch` - the source repo's own release workflow sends, or a scheduled job that - diffs the submodule's tag list. -2. On a new tag: check that submodule out at the tag (not `main`), run - *only* that tool's existing sync/Doxygen/doxygen2docusaurus steps - against it into the versioned plugin instance, then - `docusaurus docs:version: ` to snapshot it, and commit the - resulting `versioned_docs/`/`versioned_sidebars/`/`versions.json`. -3. `main` keeps being rebuilt on every push exactly as today — this only - adds new, immutable snapshots alongside it. - -**Effort:** substantial — new tag-watching trigger, splitting one tool's -docs into their own plugin instance (URL and cross-link changes), -a version picker, and every snapshot's generated output committed to the -repo permanently (a full Doxygen tree isn't small; growth is unbounded and -worth watching). - -**Real payoff:** the one thing option A can't give — an old-version reader -actually sees docs that match their version, in place, on this site. - -**Recommendation if we go here:** pilot on the C++ API reference alone -before deciding whether it's worth repeating per language/tool. Don't build -this for all three submodules on day one — that's the combinatorial problem -again, just self-inflicted. - -### C — Push versioning out to each source repo instead - -Rather than reconciling N repos' version histories into one site, don't try -— let each tool repo own publishing its own versioned docs (its own -GitHub Pages/`gh-pages` branch, or a Doxygen HTML artifact attached to each -GitHub Release), and have this site link out to it per tool, the same way -option A's changelog link does but with an actual matching-version doc on -the other end instead of just a diff to read. - -**Effort:** small-to-medium *per repo* that does this (each tool repo needs -its own per-tag publish step — real work, just not work that touches this -site's own pipeline), and **zero** structural change here beyond a link. - -**Trade-off vs. B:** a less unified experience (leaving this site to read -another one), against not adding another versioning subsystem to an -aggregator that already carries a lot of custom pipeline complexity -(`sync-external-docs.js` alone is substantial). Worth taking seriously -precisely *because* this repo is already doing a lot of bespoke work just -to aggregate "latest" — every additional thing centralized here is another -thing four repos' worth of change has to stay compatible with. - -### D — Periodic whole-site snapshots (coarse) - -Accept that a precise per-tag match isn't achievable centrally, and instead -snapshot the *entire* site occasionally (Docusaurus's own `docs:version`, -used as designed, against the whole `docs/` tree) — e.g. whenever a -coordinated multi-product release happens, or on a fixed cadence like -quarterly. One label like "docs as of 2026-Q3" captures every product -together, imprecisely but honestly. - -**Effort:** small — this is what Docusaurus's versioning feature is -actually built for; the only real decision is *when* to cut one. - -**Trade-off:** doesn't answer "show me exactly grippers v2.1's docs" — -answers "show me roughly what the docs looked like when I got this -hardware," which is a real, if fuzzier, question some customers have. Bakes -in every product's state at once, including ones the reader doesn't care -about. - -### E — Three entries in one dropdown: Latest, Stable, Previous versions - -The direction we're actually taking, combining D's "whole-site, not -per-repo-tag" shape with A and C: instead of one build tracking `main`, -run **exactly two real builds, forever**, plus one deliberately-not-real -third entry in the same version dropdown — - -- **Latest** — today's behavior, unchanged. Every submodule pinned to its - default branch tip, rebuilt on every push, exactly as now. -- **Stable** — every submodule pinned to its own *newest tag* instead of - `main`, rebuilt whenever any of those tags move. -- **Previous versions** — not a real docs build at all. Selecting it shows - a single signpost page: "we only host Latest and Stable here — for an - older release, see that product's own tagged source," with one link per - submodule straight to its GitHub tags/releases page. This is option C, - surfaced exactly where a reader who suspects a version mismatch already - looks — the version dropdown — instead of a small link buried on each - tool page. - -Latest and Stable are live/continuously regenerated (unlike D's frozen -periodic snapshots) — "Stable" isn't a point-in-time archive, it's a -second, independent build of the same pipeline pointed at a different ref -per submodule. There is still no single meaningful "version number" for the -site as a whole (grippers v2.1 + tactile_sensors v1.4 still aren't "the -same version" of anything), so "Stable" gets a name, not a version number — -it answers "give me whatever's actually been released," which is what most -readers on a tagged dependency actually want, without us having to solve -"which exact combination of N tags does this reader have." - -This is deliberately bounded: at most 2 *real* builds, never N, so it -doesn't reintroduce the combinatorial problem B and the original worry were -about — "Previous versions" costs nothing per submodule tag, since it's one -static page, not a rebuild. What it gives up versus B is precision for -anyone *behind* the latest tag (a reader on grippers v1.0 when v3.0 is -current "Stable" still won't see matching docs *here*) — that's exactly -who the third entry is for, pointed outward to where that precision -actually lives (the source repo's own tag). - -**Scope: the dropdown only appears on pages backed by a submodule we -actually pin and tag-track.** That's narrower than "generated" — it's -specifically each tool's own folder (its `index.mdx`, `_readme.md`, -`API/`, `docs/`) for a **Robotiq-maintained** tool -(`Supported_by-Robotiq-blue`, synced via a real `external-jobs.js` job). -It does *not* appear on: - -- Product/ROS/Simulation/Other landing pages — these aggregate several - tools' badges (`generate-tools-table.js` output) and aren't any one - submodule's content. -- **Third-party tool pages** (`Supported_by-Third_party-lightgrey`) — - pyRobotiqGripper, community ROS packages, PyBullet, MuJoCo, GraspGen, - and so on. We don't pin or tag-track these at all, so there's no "Stable" - or "Previous" *of* them to offer — they keep showing the one - hand-written blurb + external link they show today, unconditionally. -- `docs/contribute/` and everything else hand-authored outside a tool's own - folder. - -## Decision - -1. **Ship A now**, on both real streams once E exists. Cheap, and "Latest" - in particular needs the "this may include unreleased changes" framing A - gives it once there's a "Stable" alternative sitting right next to it. -2. **Build E**, replacing the plain "Do B if it ever comes up" posture — - this is the actual answer, not a fallback. See the implementation sketch - below. -3. **C is the answer for "older than Stable,"** delivered as E's third - dropdown entry rather than a link buried on each tool page — a reader - who suspects a version mismatch reaches for the version switcher first, - so that's where "go check the source repo's tag" belongs. -4. **D and full per-tag B stay parked.** E already gives most of B's value - (an actually-released version to read) without its cost, and D's - "fuzzier, coarser" framing is subsumed by E's "Stable" for anyone who - just wants *a* released state rather than a point-in-time archive. - -## Implementation sketch (E) - -- **Pin tracking for "Stable":** `dependabot.yml`'s `gitsubmodule` - ecosystem only tracks each submodule's default branch — it has no - "track latest tag" mode. Needs a new, small piece analogous to - `scripts/check-submodule-pins.js`: on a schedule, `git ls-remote --tags` - each submodule's repo, pick the newest (semver-sorted) tag, and open a PR - bumping a *second* set of pins if it moved. Where those second pins live - depends on how the two builds are separated (see next point) — most - likely a second `.gitmodules`-equivalent or a config file - `scripts/external-jobs.js` can read a `ref` override from, since plain - git submodules only carry one pin per path and can't represent "this - path, two different commits" at once. -- **One small Docusaurus docs-plugin instance per Robotiq-maintained tool** - (not one big instance for the whole site) — e.g. `Adaptive grippers` × - `C++`, `Adaptive grippers` × `Python`, `Tactile Sensor` × `C++`, and so - on, rooted at that tool's own existing folder. This is *how* the - dropdown ends up scoped correctly: it's a structural consequence of - which plugin instance a page belongs to, not a visibility rule bolted on - afterward. Product/ROS/aggregation pages and every third-party tool page - simply stay on the single default (unversioned) instance they're on - today, untouched. -- **Version each instance for real, three entries, stock component:** - Docusaurus's own multi-version support (`versions.json` + the built-in - version-dropdown navbar item) applied to each of those small instances, - with three entries — "Next" (Latest), "Stable", and "Previous versions" - as a genuine third version whose content is just the signpost page below. - No swizzling, no custom dropdown: reusing the stock component on a - narrowly-scoped instance is what gets us "only on the relevant pages" for - free, so there's no separate mechanism left to build for that. - `deploy.yml` needs a build pass per instance for the "Stable" pins, - alongside the existing "Latest" build. -- **The signpost page's content** is one list, one row per submodule: - product/tool name → link to that repo's GitHub tags or Releases page. - Generatable from the same `repoUrl` `external-jobs.js` already has per - submodule, so it never goes stale as tools are added. -- **A's stamping applies to both real streams** — "Latest" should say - plainly that it can be ahead of anything released; "Stable" should say - which tag of each submodule it reflects, since "Stable" is still an - aggregate and a reader may only care about one of the products in it. - -## Open questions - -- Pin-tracking mechanics: is a second `external-jobs.js`-style config - (declaring a `ref` per submodule, defaulting to "latest tag") simpler - than trying to make one `.gitmodules` represent two states? Needs a - design pass of its own before this is buildable. -- Semver-sorting tags assumes every submodule tags `vX.Y.Z` consistently — - worth confirming, since a repo that tags inconsistently (or not at all - yet) breaks "pick the newest tag" silently. -- Does "Stable" rebuild only when a tag moves, or also whenever `main` - rebuilds (cheap, always in sync, more CI minutes) — same trade-off - `deploy.yml`'s new cron backstop already made once for "Latest." -- One small plugin instance per tool means one more entry to add whenever - a new tool is onboarded — worth folding into `adding-a-tool.mdx`'s - existing checklist so it's not a step someone forgets, the same way that - checklist already covers `external-jobs.js` and `sidebars.js`. diff --git a/scripts/external-jobs.js b/scripts/external-jobs.js index 34d303c..6f406c8 100644 --- a/scripts/external-jobs.js +++ b/scripts/external-jobs.js @@ -12,7 +12,7 @@ function submoduleJobs(submodule, { repoUrl, branch }, jobs) { const JOBS = [ ...submoduleJobs('2f85_cpp', { repoUrl: 'https://github.com/robotiq/grippers', branch: 'main' }, [ // Every job below has its own versioned plugin instance (see - // draft/documentation-versioning.md, and docusaurus.config.js's + // docs/contribute/versioning.mdx, and docusaurus.config.js's // 'adaptive-grippers-cpp' entry) — destRoot per the comment on it in // sync-external-docs.js's job loop, so `to` here is relative to // versioned-tools/ instead of docs/. @@ -94,6 +94,12 @@ const JOBS = [ // destRoot doxygen2docusaurus job needs this explicitly (its own // absolute-slug generation can't derive it from destRoot alone). routeBasePath: '/docs/drivers/Adaptive grippers/Libraries/C++', + // Must match this tool's own `versions.current.path` in + // docusaurus.config.js exactly — this job always writes the + // *current* version's content, and Development (main) lives under + // '/next' now that Stable owns the instance root. See the comment on + // this field in sync-external-docs.js. + currentVersionPath: 'next', exclude: [ 'files', 'folders', 'indices/files', 'namespaces', 'indices/namespaces', @@ -121,7 +127,7 @@ const JOBS = [ ...submoduleJobs('tactile_sensors', { repoUrl: 'https://github.com/robotiq/tactile_sensors', branch: 'main' }, [ // TSF 85 CPP and Python driver READMEs — both under per-tool - // documentation versioning (see draft/documentation-versioning.md), so + // documentation versioning (see docs/contribute/versioning.mdx), so // both live outside docs/ in their own destRoot: nesting a second, // separately versioned Docusaurus plugin instance's files inside the // main docs/ tree (even excluded from it) breaks MDX compilation — see @@ -136,12 +142,13 @@ const JOBS = [ // Robotiq's own 2F gripper Isaac Sim assets/guide — no README, just this // one guide file (no separate docs/ split yet: nothing else to put there). // Under per-tool documentation versioning (see - // draft/documentation-versioning.md) like every other Robotiq- + // docs/contribute/versioning.mdx) like every other Robotiq- // maintained, submodule-synced tool — destRoot per the comment on it in // sync-external-docs.js. isaacsim_assets has no tags yet, so its - // plugin instance currently only has a 'Latest' version (no Stable/ - // Previous versions cut) — becomes a real 3-way switcher automatically - // once it gets its first tag, no restructuring needed then. + // plugin instance currently only has its current (Development/main) + // content (no Stable/Previous versions cut) — becomes a real 3-way + // switcher automatically once it gets its first tag, no restructuring + // needed then. { from: 'grippers/GRIPPER_SIMULATION_GUIDE.md', to: 'Adaptive grippers/Simulation/Isaac Sim/_readme.md', destRoot: 'versioned-tools' }, ]), ]; diff --git a/scripts/folder-sidebar.mjs b/scripts/folder-sidebar.mjs index 358d542..7e9184f 100644 --- a/scripts/folder-sidebar.mjs +++ b/scripts/folder-sidebar.mjs @@ -44,7 +44,7 @@ function stripNumberPrefix(filename) { * folder that's the versioned-instance root of its own tool. * @param {string} [fsRoot] Filesystem root `docPrefix` is relative to — * defaults to the default instance's own `docs/`. A tool with its own - * versioned plugin instance (see draft/documentation-versioning.md) + * versioned plugin instance (see docs/contribute/versioning.mdx) * passes its own `versioned-tools//` root instead, since * its doc-id namespace starts there, not at the site's `docs/`. */ diff --git a/scripts/generate-tools-table.js b/scripts/generate-tools-table.js index 024cd7f..0d57783 100644 --- a/scripts/generate-tools-table.js +++ b/scripts/generate-tools-table.js @@ -34,7 +34,7 @@ const matter = require('gray-matter'); const ROOT = path.resolve(__dirname, '..'); const DRIVERS_DIR = path.join(ROOT, 'docs', 'drivers'); // A tool piloting per-tool documentation versioning (see -// draft/documentation-versioning.md) lives here instead of under +// docs/contribute/versioning.mdx) lives here instead of under // DRIVERS_DIR — its own Docusaurus plugin instance can't be nested inside // the main docs/ tree (see the destRoot comment in sync-external-docs.js). // Mirrors DRIVERS_DIR's own /<...> structure underneath each diff --git a/scripts/list-submodule-tags.js b/scripts/list-submodule-tags.js index ae33ee6..32798f9 100644 --- a/scripts/list-submodule-tags.js +++ b/scripts/list-submodule-tags.js @@ -1,6 +1,6 @@ #!/usr/bin/env node // Lists a submodule repo's tags, newest first — the building block for two -// things described in draft/documentation-versioning.md: +// things described in docs/contribute/versioning.mdx: // - picking the *newest* tag to pin the "Stable" version to // - listing the *older* tags on a tool's "Previous versions" signpost // page, each linking straight to that tag's source @@ -19,8 +19,8 @@ const JOBS = require('./external-jobs'); // Only vN / vN.N / vN.N.N-style tags sort meaningfully against each other — // anything else (a stray 'latest', a pre-release branch tag, ...) is left -// out rather than guessed at. Matches the semver-sort assumption already -// flagged as an open question in draft/documentation-versioning.md. +// out rather than guessed at. This assumes every submodule tags `vX.Y.Z` +// consistently — true for every submodule this repo currently pins. const SEMVER_TAG_RE = /^v(\d+)(?:\.(\d+))?(?:\.(\d+))?$/; function compareSemver(a, b) { diff --git a/scripts/site-nav-tree.mjs b/scripts/site-nav-tree.mjs index d22909e..16641b6 100644 --- a/scripts/site-nav-tree.mjs +++ b/scripts/site-nav-tree.mjs @@ -7,7 +7,7 @@ // Why this exists: each versioned tool (Tactile Sensor C++/Python, Isaac // Sim, Adaptive grippers C++) lives in its own Docusaurus // plugin-content-docs instance for independent version cuts (see -// docusaurus.config.js, draft/documentation-versioning.md). A plugin +// docusaurus.config.js, docs/contribute/versioning.mdx). A plugin // instance can only build sidebar items out of doc ids it owns — every // other page has to be a plain link. Previously each per-tool sidebar file // listed ONLY that tool's own page(s), so clicking a versioned tool from @@ -218,7 +218,7 @@ export function buildInstanceSidebar(activeTool, activeItem) { // (a versioned tool's non-current versions can have a DIFFERENT shape // than its current one — e.g. adaptive-grippers-cpp's Stable is a single // page today, sparse content from before its source repo grew a docs/ -// folder, while its Latest has nested guides/API). Rather than guess that +// folder, while its Development (main) has nested guides/API). Rather than guess that // shape, this walks SITE_TREE in lockstep with the version's OWN existing // snapshot and pulls out whatever's already sitting at `activeTool`'s // position — correct by construction, since that position held the real, diff --git a/scripts/sync-external-docs.js b/scripts/sync-external-docs.js index 7999819..357d800 100644 --- a/scripts/sync-external-docs.js +++ b/scripts/sync-external-docs.js @@ -53,7 +53,7 @@ const JOBS = require('./external-jobs'); // site's real mount point, e.g. 'drivers/Adaptive grippers/Libraries/C++/API'), // so nothing needs post-hoc rewriting: the file lands at exactly the path // its own baked-in links already assume. A job with `destRoot` (its own -// versioned plugin instance — see draft/documentation-versioning.md) is +// versioned plugin instance — see docs/contribute/versioning.mdx) is // different: `apiBaseUrl` there is just the instance-relative tail (e.g. // 'API'), since the instance's own `routeBasePath` already supplies // everything before it — doxygen2docusaurus's own slug/id construction @@ -316,6 +316,31 @@ function injectTitleFromH1(content) { return content.replace(fmMatch[0], newFrontmatter); } +// This job (doxygen2docusaurus) only ever writes the CURRENT (Development +// /main) version's content — a cut Stable/Previous-versions snapshot is a +// frozen copy taken later, never regenerated by this pipeline — so every +// page it touches documents unreleased, non-committed APIs by definition. +// Inserted right after the frontmatter block, before any real content, so +// it's the first thing visible regardless of which specific class/group +// page a reader lands on directly (e.g. from a search result or a +// deep-link) — not just the API section's own top-level landing page. See +// docs/api-stability.mdx for the full policy this links to. +function injectExperimentalNotice(content) { + const fmMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/); + if (!fmMatch) return content; + const notice = [ + '', + ':::tip Experimental', + 'This documents unreleased `main` — not a released, committed API. See', + 'the [API stability policy](/docs/api-stability) and use **Stable**', + 'instead.', + ':::', + '', + '', + ].join('\n'); + return content.slice(0, fmMatch[0].length) + notice + content.slice(fmMatch[0].length); +} + // A group/class/struct page's own top-level description (as opposed to a // member's) is split in two by doxygen2docusaurus, same as classic Doxygen // HTML: a short brief right under the title with a "More..." jump link, then @@ -1157,7 +1182,22 @@ function runDoxygen2Docusaurus(job, written, folderDestPaths) { currentDocsRoot = job.destRoot ? path.join(ROOT, job.destRoot, path.dirname(job.to)) : path.join(ROOT, 'docs'); - currentRoutePrefix = job.destRoot ? job.routeBasePath : '/docs'; + // This job always writes to the CURRENT (live, always-rebuilt) version's + // own content — a cut version is a frozen copy taken later, never + // written to directly by this pipeline. If that current version's own + // `path` in docusaurus.config.js's `versions` config isn't the instance + // root ('') — e.g. 'next', once the root path was handed to Stable + // instead (see "Which version the root URL serves" in + // docs/contribute/versioning.mdx) — every absolute `` + // backlink doxygen2docusaurus bakes into the generated HTML needs that + // same segment, or it 404s: Docusaurus's router adds a version's own + // path segment on top of the plugin's routeBasePath, but + // doxygen2docusaurus's own link generation has no idea versioning even + // exists, so it only ever uses the bare routeBasePath. `job.currentVersionPath` + // must be kept in sync with that tool's own `versions.current.path`. + currentRoutePrefix = job.destRoot + ? job.routeBasePath + (job.currentVersionPath ? `/${job.currentVersionPath}` : '') + : '/docs'; const doxygenXmlInputFolderPath = path .relative(ROOT, doxygenXmlDirAbs) @@ -1249,7 +1289,7 @@ function runDoxygen2Docusaurus(job, written, folderDestPaths) { repoUrl: job.repoUrl, branch: job.branch, rawCopy: true, - transformContent: (content) => injectTitleFromH1(moveDetailedDescriptionToTop(stripDeadDoxygenLinks(splitMemberSignatures(improveTitleAndStripDeclaration(mergeMemberIndexTables(stripLocationParagraphs(stripPrivateMemberSections(content))))), apiFolderPath, job.exclude))), + transformContent: (content) => injectExperimentalNotice(injectTitleFromH1(moveDetailedDescriptionToTop(stripDeadDoxygenLinks(splitMemberSignatures(improveTitleAndStripDeclaration(mergeMemberIndexTables(stripLocationParagraphs(stripPrivateMemberSections(content))))), apiFolderPath, job.exclude)))), }, stagingApiDir, written); folderDestPaths.add(destPath); stripDanglingAnchorLinksAcrossFiles(destPath); @@ -1731,7 +1771,7 @@ for (const job of JOBS) { // Defaults to 'docs' for every existing job. A job whose content needs to // live outside the main Docusaurus docs instance's own tree — e.g. a tool // piloting per-tool documentation versioning (see - // draft/documentation-versioning.md) as its own separate plugin instance + // docs/contribute/versioning.mdx) as its own separate plugin instance // — sets destRoot explicitly instead. Physically nesting that instance's // files inside docs/ (even with the default instance's own `exclude` // covering them) reproducibly breaks MDX compilation with a bogus diff --git a/sidebars.adaptive-grippers-cpp.js b/sidebars.adaptive-grippers-cpp.js index 04fc209..4fa08e3 100644 --- a/sidebars.adaptive-grippers-cpp.js +++ b/sidebars.adaptive-grippers-cpp.js @@ -2,7 +2,7 @@ // Sidebar for the 'adaptive-grippers-cpp' plugin instance only (see // docusaurus.config.js's `plugins` array and -// draft/documentation-versioning.md) — mirrors the "Introduction guides" + +// docs/contribute/versioning.mdx) — mirrors the "Introduction guides" + // "API Reference" nesting the main sidebars.js used to carry for this tool // directly, before it moved to its own versioned instance. Doc ids here are // relative to this instance's own `path` diff --git a/sidebars.isaac-sim.js b/sidebars.isaac-sim.js index 2a40b5f..4842779 100644 --- a/sidebars.isaac-sim.js +++ b/sidebars.isaac-sim.js @@ -2,7 +2,7 @@ // Sidebar for the 'isaac-sim' plugin instance only (see // docusaurus.config.js's `plugins` array and -// draft/documentation-versioning.md). Built from the same shared tree as +// docs/contribute/versioning.mdx). Built from the same shared tree as // the main sidebar (scripts/site-nav-tree.mjs) so the rest of the site's // navigation stays visible here too — only this tool's own node resolves // to real content (a single page, no guides/API sub-navigation); every diff --git a/sidebars.js b/sidebars.js index a0e26f9..949c056 100644 --- a/sidebars.js +++ b/sidebars.js @@ -7,7 +7,7 @@ // for the Adaptive grippers C++ tool's "Introduction guides"/"API // Reference" nesting — that tool moved to its own versioned plugin instance // (see docusaurus.config.js, 'adaptive-grippers-cpp', and -// draft/documentation-versioning.md), taking that nesting with it into +// docs/contribute/versioning.mdx), taking that nesting with it into // sidebars.adaptive-grippers-cpp.js. Nothing left in this file needs them; // re-add if a future non-versioned tool grows the same guides+API shape. // @@ -32,7 +32,12 @@ import { buildMainSidebar } from './scripts/site-nav-tree.mjs'; @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - driverSidebar: buildMainSidebar(), + // 'api-stability' is appended directly rather than folded into + // SITE_TREE: it's a general policy page linked contextually from the + // Development (main) version banner and docs/intro.mdx, not a + // product/tool — it doesn't need to appear on every versioned tool's own + // sidebar the way SITE_TREE's shared shape does. + driverSidebar: [...buildMainSidebar(), 'api-stability'], // Contributor docs — deliberately not shown in the site's main navbar // (Docusaurus still uses this sidebar whenever someone lands on a diff --git a/sidebars.tactile-cpp.js b/sidebars.tactile-cpp.js index 20d5bc7..42b48c4 100644 --- a/sidebars.tactile-cpp.js +++ b/sidebars.tactile-cpp.js @@ -2,7 +2,7 @@ // Sidebar for the 'tactile-cpp' plugin instance only (see // docusaurus.config.js's `plugins` array and -// draft/documentation-versioning.md). Built from the same shared tree as +// docs/contribute/versioning.mdx). Built from the same shared tree as // the main sidebar (scripts/site-nav-tree.mjs) so the rest of the site's // navigation stays visible here too — only this tool's own node resolves // to real content (a single page, no guides/API sub-navigation); every diff --git a/sidebars.tactile-python.js b/sidebars.tactile-python.js index d7fe221..e2f8ff3 100644 --- a/sidebars.tactile-python.js +++ b/sidebars.tactile-python.js @@ -2,7 +2,7 @@ // Sidebar for the 'tactile-python' plugin instance only (see // docusaurus.config.js's `plugins` array and -// draft/documentation-versioning.md). Built from the same shared tree as +// docs/contribute/versioning.mdx). Built from the same shared tree as // the main sidebar (scripts/site-nav-tree.mjs) so the rest of the site's // navigation stays visible here too — only this tool's own node resolves // to real content (a single page, no guides/API sub-navigation); every diff --git a/src/theme/DocVersionBanner/index.jsx b/src/theme/DocVersionBanner/index.jsx index 8122460..5a8f930 100644 --- a/src/theme/DocVersionBanner/index.jsx +++ b/src/theme/DocVersionBanner/index.jsx @@ -13,10 +13,25 @@ import {ThemeClassNames} from '@docusaurus/theme-common'; // hardcodes two stacked paragraphs ("This is unreleased documentation for // ... version." then, on its own line, "For up-to-date documentation, see // the latest version (...).") — condensed to one line/one sentence here. -const BANNER_TEXT = { - unreleased: 'Unreleased documentation — for the latest release, see', - unmaintained: 'No longer maintained — for the latest release, see', -}; +// +// The 'unreleased' text also states this site's actual API-stability +// policy, not just "unreleased" — Development (main) documents +// in-progress work, and a reader landing here (e.g. from a search result, +// before `noIndex` on that version took effect everywhere it's indexed) +// needs to know its APIs aren't a commitment, not just that it's "not the +// latest" — see docs/api-stability.mdx for the full policy this links to. +function BannerText({banner}) { + if (banner === 'unreleased') { + return ( + <> + Experimental: documents unreleased main. APIs may + change or be removed without notice and aren't covered by our{' '} + compatibility commitment. Use + + ); + } + return <>No longer maintained — for the latest release, see; +} function DocVersionBannerEnabled({className, versionMetadata}) { const {pluginId} = useActivePlugin({failfast: true}); @@ -29,7 +44,7 @@ function DocVersionBannerEnabled({className, versionMetadata}) {
- {BANNER_TEXT[versionMetadata.banner]}{' '} + {' '} - 2F-85 gripper + C++ logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) ![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) -The source files of the TSF CPP driver developed and maintained by Robotiq -team is available the the following repository: +The source files of the TSF C++ driver, developed and maintained by the +Robotiq team, are available in the following repository: -https://github.com/robotiq/tactile_sensors/tree/main/sdk_cpp +https://github.com/robotiq/tactile_sensors/tree/v2.0.0/sdk_cpp -Here below are the related instructions to use this driver. +Below are the related instructions to use this driver. ## Intro diff --git a/tactile-python_versioned_docs/version-previous-versions/index.mdx b/tactile-python_versioned_docs/version-previous-versions/index.mdx index d3a7e6d..c2b4400 100644 --- a/tactile-python_versioned_docs/version-previous-versions/index.mdx +++ b/tactile-python_versioned_docs/version-previous-versions/index.mdx @@ -5,7 +5,7 @@ sidebar_label: Python **Stable** currently tracks `tactile_sensors`'s newest release, **v2.0.0**. -We only host **Latest** and **Stable** here — older releases aren't +We only host **Development (main)** and **Stable** here — older releases aren't archived on this site. Browse their own tag in the source repository instead: diff --git a/tactile-python_versioned_docs/version-stable/index.mdx b/tactile-python_versioned_docs/version-stable/index.mdx index 583e6be..c07e643 100644 --- a/tactile-python_versioned_docs/version-stable/index.mdx +++ b/tactile-python_versioned_docs/version-stable/index.mdx @@ -4,19 +4,19 @@ sidebar_label: Python ---
- 2F-85 gripper + Python logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) ![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) -Example of TSF usage via python are available developped and maintained by Robotiq -team is available the the folowing repository: +Examples of TSF usage via Python, developed and maintained by the Robotiq +team, are available in the following repository: -https://github.com/robotiq/tactile_sensors/tree/main/sensor_quickstart +https://github.com/robotiq/tactile_sensors/tree/v2.0.0/sensor_quickstart -Are below are the related instructions to use this driver. +Below are the related instructions to use this driver. ## Intro diff --git a/versioned-tools/Adaptive grippers/Libraries/C++/index.mdx b/versioned-tools/Adaptive grippers/Libraries/C++/index.mdx index ee7626f..0fbe4e8 100644 --- a/versioned-tools/Adaptive grippers/Libraries/C++/index.mdx +++ b/versioned-tools/Adaptive grippers/Libraries/C++/index.mdx @@ -4,13 +4,20 @@ sidebar_label: C++ ---
- 2F-85 gripper + C++ logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) ![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) +:::tip Use a released version +This page documents unreleased `main`. Install a tagged release instead — +see **Stable** in the version switcher above, or this repository's own +tags on GitHub. `main` contains in-progress work; its API is not a +commitment — see the [API stability policy](/docs/api-stability). +::: + import Readme from './_readme.md'; diff --git a/versioned-tools/Tactile Sensor/Libraries/C++/index.mdx b/versioned-tools/Tactile Sensor/Libraries/C++/index.mdx index b9c0971..441c124 100644 --- a/versioned-tools/Tactile Sensor/Libraries/C++/index.mdx +++ b/versioned-tools/Tactile Sensor/Libraries/C++/index.mdx @@ -4,19 +4,26 @@ sidebar_label: C++ ---
- 2F-85 gripper + C++ logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) ![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) -The source files of the TSF CPP driver developed and maintained by Robotiq -team is available the the following repository: +:::tip Use a released version +This page documents unreleased `main`. Install a tagged release instead — +see **Stable** in the version switcher above, or this repository's own +tags on GitHub. `main` contains in-progress work; its API is not a +commitment — see the [API stability policy](/docs/api-stability). +::: + +The source files of the TSF C++ driver, developed and maintained by the +Robotiq team, are available in the following repository: https://github.com/robotiq/tactile_sensors/tree/main/sdk_cpp -Here below are the related instructions to use this driver. +Below are the related instructions to use this driver. ## Intro diff --git a/versioned-tools/Tactile Sensor/Libraries/Python/index.mdx b/versioned-tools/Tactile Sensor/Libraries/Python/index.mdx index 583e6be..6fd9f73 100644 --- a/versioned-tools/Tactile Sensor/Libraries/Python/index.mdx +++ b/versioned-tools/Tactile Sensor/Libraries/Python/index.mdx @@ -4,19 +4,26 @@ sidebar_label: Python ---
- 2F-85 gripper + Python logo
![Libraries](https://img.shields.io/badge/Category-Libraries-lightgrey) ![Supported by Robotiq](https://img.shields.io/badge/Supported_by-Robotiq-blue) -Example of TSF usage via python are available developped and maintained by Robotiq -team is available the the folowing repository: +:::tip Use a released version +This page documents unreleased `main`. Install a tagged release instead — +see **Stable** in the version switcher above, or this repository's own +tags on GitHub. `main` contains in-progress work; its API is not a +commitment — see the [API stability policy](/docs/api-stability). +::: + +Examples of TSF usage via Python, developed and maintained by the Robotiq +team, are available in the following repository: https://github.com/robotiq/tactile_sensors/tree/main/sensor_quickstart -Are below are the related instructions to use this driver. +Below are the related instructions to use this driver. ## Intro From 7846b8bc746ab965b649dcec06d910d0f9ad8652 Mon Sep 17 00:00:00 2001 From: bcastets-robotiq Date: Mon, 28 Sep 2026 09:33:59 -0400 Subject: [PATCH 5/5] Fix CI failure: node --test glob pattern not supported on Node 20 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test:unit failed on CI (ubuntu-latest, Node 20) with "Could not find '.../test/**/*.test.js'" — node --test's own glob-matching for positional args, which the local dev environment's Node 24 supports, isn't available on Node 20. A bare directory argument (node --test test/, closer to the reviewer's original suggestion) isn't safe either: it reproducibly fails locally (Windows, Node 24) with an unrelated "Cannot find module" error, untested on Linux/Node 20. Replaced both with scripts/run-unit-tests.js: discovers test/*.test.{js,mjs} via a plain fs.readdirSync and passes the file list to node:test's programmatic run() API. No CLI glob or directory-argument ambiguity, works identically regardless of OS or Node version. Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- scripts/run-unit-tests.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 scripts/run-unit-tests.js diff --git a/package.json b/package.json index d1269aa..2d4c4e7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "cross-env NODE_OPTIONS=--max-old-space-size=8192 docusaurus start", "prebuild": "npm run generate", "build": "docusaurus build", - "test:unit": "node --test \"test/**/*.test.js\" \"test/**/*.test.mjs\"", + "test:unit": "node scripts/run-unit-tests.js", "test": "node scripts/check-build.js", "swizzle": "docusaurus swizzle", "clear": "docusaurus clear", diff --git a/scripts/run-unit-tests.js b/scripts/run-unit-tests.js new file mode 100644 index 0000000..0cb3e9e --- /dev/null +++ b/scripts/run-unit-tests.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +// Runs every test/*.test.{js,mjs} file via node:test's programmatic run() +// API, discovering files with a plain fs.readdirSync instead of a CLI +// glob/directory argument. +// +// `node --test test/` (the CLI's own directory-discovery form) is what +// Node's docs recommend, but a bare directory argument reproducibly failed +// on this machine (Windows, Node 24) with a confusing "Cannot find module" +// error — and CI (ubuntu-latest, Node 20, a different OS *and* a different +// Node major) is exactly the kind of environment that difference could +// go either way in, untested. A hand-typed glob string +// ('test/**/*.test.js') isn't safe either: that syntax only works because +// newer Node versions added their own internal glob matching for CLI +// positional args — confirmed by testing, it fails outright on Node 20 +// with "Could not find '.../test/**/*.test.js'" (took the string as a +// literal, non-existent path). Explicit file discovery here sidesteps +// both: no directory-argument ambiguity, no glob-support version +// dependency, works identically regardless of OS or Node 20 vs 24. +const path = require('node:path'); +const {run} = require('node:test'); +const {spec: Spec} = require('node:test/reporters'); + +const TEST_DIR = path.join(__dirname, '..', 'test'); +const fs = require('node:fs'); + +const files = fs + .readdirSync(TEST_DIR) + .filter((f) => /\.test\.(js|mjs)$/.test(f)) + .map((f) => path.join(TEST_DIR, f)); + +const stream = run({files}); +stream.compose(new Spec()).pipe(process.stdout); +stream.on('test:fail', () => { + process.exitCode = 1; +});