Skip to content

cdn-swap-npm#263

Open
aidenybai wants to merge 1 commit intomainfrom
cdn-swap-npm
Open

cdn-swap-npm#263
aidenybai wants to merge 1 commit intomainfrom
cdn-swap-npm

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented Mar 24, 2026

  • 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.

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 DISTRIBUTION build-time env (cdn vs npm) in tsup and, for npm distribution only, adds a runtime auto-update path that disposes the current instance and injects the latest index.global.js from unpkg, 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 from unpkg when a newer release is available.

  • New Features
    • tsup config now builds both distributions and sets DISTRIBUTION accordingly (cdn or npm).
    • New fetchLatestVersion utility pulls the latest version from react-grab.com, compares semver, and skips when offline or in extensions.
    • npm: on init, if newer version exists, dispose current API and inject https://unpkg.com/react-grab@<version>/dist/index.global.js (fallback to local on error).
    • CDN: intro log uses fetchLatestVersion to warn when the loaded version is outdated.

Written for commit 0fce6db. Summary will update on new commits.

…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.
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-website Ready Ready Preview, Comment Mar 24, 2026 1:04pm

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 24, 2026

Open in StackBlitz

@react-grab/cli

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@263

grab

npm i https://pkg.pr.new/aidenybai/react-grab/grab@263

@react-grab/amp

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/amp@263

@react-grab/claude-code

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/claude-code@263

@react-grab/codex

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/codex@263

@react-grab/copilot

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/copilot@263

@react-grab/cursor

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cursor@263

@react-grab/droid

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/droid@263

@react-grab/gemini

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/gemini@263

@react-grab/opencode

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/opencode@263

react-grab

npm i https://pkg.pr.new/aidenybai/react-grab@263

@react-grab/relay

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/relay@263

@react-grab/utils

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/utils@263

commit: 0fce6db

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

};
document.head.appendChild(script);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

};
document.head.appendChild(script);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

);

if (process.env.DISTRIBUTION === "npm" && globalApi) {
void fetchLatestVersion().then((latestVersion) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing handler for successful script load in NPM distribution version update flow causes stale globalApi state and no event notification when newer version loads from unpkg

Fix on Vercel

Comment on lines +4 to +11
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Fix on Vercel

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

script.src = `https://unpkg.com/react-grab@${latestVersion}/dist/index.global.js`;
script.onerror = () => {
script.remove();
setGlobalApi(init());
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant