Conversation
…grab - Modified tsup configuration to support both "cdn" and "npm" distributions. - Enhanced version fetching logic in index.ts to dynamically load the latest version from unpkg for npm distribution. - Updated log-intro.ts to utilize fetchLatestVersion for version checks, improving consistency in version management.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@react-grab/cli
grab
@react-grab/amp
@react-grab/claude-code
@react-grab/codex
@react-grab/copilot
@react-grab/cursor
@react-grab/droid
@react-grab/gemini
@react-grab/opencode
react-grab
@react-grab/relay
@react-grab/utils
commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| }; | ||
| document.head.appendChild(script); | ||
| }); | ||
| } |
There was a problem hiding this comment.
CDN swap permanently loses all registered plugins
High Severity
The CDN swap disposes globalApi, destroying all registered plugins, but pendingPlugins was already emptied by flushPendingPlugins at initialization. Neither the CDN success path (which runs in a fresh IIFE scope with its own empty pendingPlugins) nor the onerror fallback (which calls bare init()) has any way to restore those plugins. Any plugins registered via registerPlugin() before or during initialization are silently and permanently lost when a newer version is detected.
Additional Locations (1)
| }; | ||
| document.head.appendChild(script); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Swap invalidates API for once-only event listeners
High Severity
The react-grab:init event is dispatched with the initial globalApi at line 115, which the CDN swap later disposes. Consumers like MCP and Relay clients listen with { once: true }, so they capture the first (soon-to-be-disposed) API reference, detach their listener, and never receive the replacement API from the CDN script's own init event. Additionally, the onerror fallback never dispatches react-grab:init at all.
| ); | ||
|
|
||
| if (process.env.DISTRIBUTION === "npm" && globalApi) { | ||
| void fetchLatestVersion().then((latestVersion) => { |
| const latestParts = latest.split(".").map(Number); | ||
| const currentParts = current.split(".").map(Number); | ||
| for (let index = 0; index < 3; index++) { | ||
| const latestPart = latestParts[index] ?? 0; | ||
| const currentPart = currentParts[index] ?? 0; | ||
| if (latestPart > currentPart) return true; | ||
| if (latestPart < currentPart) return false; | ||
| } |
There was a problem hiding this comment.
| const latestParts = latest.split(".").map(Number); | |
| const currentParts = current.split(".").map(Number); | |
| for (let index = 0; index < 3; index++) { | |
| const latestPart = latestParts[index] ?? 0; | |
| const currentPart = currentParts[index] ?? 0; | |
| if (latestPart > currentPart) return true; | |
| if (latestPart < currentPart) return false; | |
| } | |
| // Parse semantic versions, extracting major.minor.patch and ignoring pre-release/metadata | |
| const parseSemver = (version: string): [number, number, number] => { | |
| // Remove metadata (e.g., "+build123") - it doesn't affect version precedence | |
| const baseVersion = version.split("+")[0]; | |
| // Remove pre-release identifier (e.g., "-beta.1") - we'll handle it separately | |
| const versionMatch = baseVersion.match(/^(\d+)\.(\d+)\.(\d+)/); | |
| if (!versionMatch) { | |
| // Fallback for non-standard versions | |
| const parts = baseVersion.split(".").map(Number); | |
| return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0]; | |
| } | |
| return [parseInt(versionMatch[1], 10), parseInt(versionMatch[2], 10), parseInt(versionMatch[3], 10)]; | |
| }; | |
| const [latestMajor, latestMinor, latestPatch] = parseSemver(latest); | |
| const [currentMajor, currentMinor, currentPatch] = parseSemver(current); | |
| if (latestMajor !== currentMajor) return latestMajor > currentMajor; | |
| if (latestMinor !== currentMinor) return latestMinor > currentMinor; | |
| if (latestPatch !== currentPatch) return latestPatch > currentPatch; | |
The isNewerSemver function improperly parses semantic versions with pre-release identifiers, metadata, or non-standard formats, causing incorrect version comparisons
There was a problem hiding this comment.
2 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/react-grab/src/index.ts">
<violation number="1" location="packages/react-grab/src/index.ts:123">
P1: The CDN swap calls `globalApi?.dispose()`, permanently destroying all registered plugins. Neither the successfully-loaded CDN script (which initializes in its own scope with an empty plugin list) nor the `onerror` fallback (which calls bare `init()`) has any mechanism to re-register the plugins that were flushed during the original initialization. Any plugins registered via `registerPlugin()` before the swap are silently and permanently lost.</violation>
<violation number="2" location="packages/react-grab/src/index.ts:130">
P1: The `onerror` fallback re-initializes the API via `init()` but never re-dispatches the `react-grab:init` event. Consumers (e.g., MCP/Relay clients) that listen with `{ once: true }` will have already captured and detached on the first dispatch, leaving them holding a reference to the now-disposed original API. They will never receive the replacement API from either the CDN success path or this error fallback.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| void fetchLatestVersion().then((latestVersion) => { | ||
| if (!latestVersion) return; | ||
|
|
||
| globalApi?.dispose(); |
There was a problem hiding this comment.
P1: The CDN swap calls globalApi?.dispose(), permanently destroying all registered plugins. Neither the successfully-loaded CDN script (which initializes in its own scope with an empty plugin list) nor the onerror fallback (which calls bare init()) has any mechanism to re-register the plugins that were flushed during the original initialization. Any plugins registered via registerPlugin() before the swap are silently and permanently lost.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/index.ts, line 123:
<comment>The CDN swap calls `globalApi?.dispose()`, permanently destroying all registered plugins. Neither the successfully-loaded CDN script (which initializes in its own scope with an empty plugin list) nor the `onerror` fallback (which calls bare `init()`) has any mechanism to re-register the plugins that were flushed during the original initialization. Any plugins registered via `registerPlugin()` before the swap are silently and permanently lost.</comment>
<file context>
@@ -114,4 +115,21 @@ if (typeof window !== "undefined" && !window.__REACT_GRAB_DISABLED__) {
+ void fetchLatestVersion().then((latestVersion) => {
+ if (!latestVersion) return;
+
+ globalApi?.dispose();
+ setGlobalApi(null);
+
</file context>
| script.src = `https://unpkg.com/react-grab@${latestVersion}/dist/index.global.js`; | ||
| script.onerror = () => { | ||
| script.remove(); | ||
| setGlobalApi(init()); |
There was a problem hiding this comment.
P1: The onerror fallback re-initializes the API via init() but never re-dispatches the react-grab:init event. Consumers (e.g., MCP/Relay clients) that listen with { once: true } will have already captured and detached on the first dispatch, leaving them holding a reference to the now-disposed original API. They will never receive the replacement API from either the CDN success path or this error fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/index.ts, line 130:
<comment>The `onerror` fallback re-initializes the API via `init()` but never re-dispatches the `react-grab:init` event. Consumers (e.g., MCP/Relay clients) that listen with `{ once: true }` will have already captured and detached on the first dispatch, leaving them holding a reference to the now-disposed original API. They will never receive the replacement API from either the CDN success path or this error fallback.</comment>
<file context>
@@ -114,4 +115,21 @@ if (typeof window !== "undefined" && !window.__REACT_GRAB_DISABLED__) {
+ script.src = `https://unpkg.com/react-grab@${latestVersion}/dist/index.global.js`;
+ script.onerror = () => {
+ script.remove();
+ setGlobalApi(init());
+ };
+ document.head.appendChild(script);
</file context>


Note
Medium Risk
Medium risk because it introduces runtime script swapping (disposing and reinitializing the global API) based on a remote version check, which can affect initialization order and failure modes in browser environments.
Overview
Adds a unified
fetchLatestVersion()helper (with semver comparison and extension/offline guards) and switches intro logging to use it for non-npm builds.Introduces a
DISTRIBUTIONbuild-time env (cdnvsnpm) intsupand, for npm distribution only, adds a runtime auto-update path that disposes the current instance and injects the latestindex.global.jsfromunpkg, with an error fallback that re-initializes locally.Written by Cursor Bugbot for commit 0fce6db. This will update automatically on new commits. Configure here.
Summary by cubic
Add dual distribution for
react-grab(CDN and npm) with dynamic version checks. npm builds can auto-load the latest version fromunpkgwhen a newer release is available.tsupconfig now builds both distributions and setsDISTRIBUTIONaccordingly (cdnornpm).fetchLatestVersionutility pulls the latest version from react-grab.com, compares semver, and skips when offline or in extensions.https://unpkg.com/react-grab@<version>/dist/index.global.js(fallback to local on error).fetchLatestVersionto warn when the loaded version is outdated.Written for commit 0fce6db. Summary will update on new commits.