Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/third-parties-react-19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'rspress-plugin-third-parties': patch
---

Remove React Compiler and its runtime dependency while preserving compatibility with React 18 and React 19.
6 changes: 4 additions & 2 deletions packages/rspress-plugin-third-parties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
[![npm version](https://img.shields.io/npm/v/rspress-plugin-third-parties.svg?style=for-the-badge&color=A855F7&labelColor=1F2937)](https://www.npmjs.com/package/rspress-plugin-third-parties)
[![npm downloads](https://img.shields.io/npm/dm/rspress-plugin-third-parties.svg?style=for-the-badge&color=3B82F6&labelColor=1F2937)](https://www.npmjs.com/package/rspress-plugin-third-parties)
[![license](https://img.shields.io/npm/l/rspress-plugin-third-parties.svg?style=for-the-badge&color=10B981&labelColor=1F2937)](https://github.com/rstackjs/rspress-plugins/blob/main/packages/rspress-plugin-third-parties/LICENSE)
[![React 19](https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge&logo=react&logoColor=black&labelColor=1F2937)](https://react.dev/)
[![React 18 & 19](https://img.shields.io/badge/React-18%20%26%2019-61DAFB?style=for-the-badge&logo=react&logoColor=black&labelColor=1F2937)](https://react.dev/)
[![Rspress](https://img.shields.io/badge/Rspress-Plugin-FF4154?style=for-the-badge&logo=rspack&logoColor=white&labelColor=1F2937)](https://rspress.rs/)
[![Docs & Demo](https://img.shields.io/badge/Docs_%26_Demo-Explore_Site-34268A?style=for-the-badge&logo=githubpages&logoColor=white&labelColor=0C1D48)](https://sanjaiyan-dev.github.io/rspress-plugin-third-parties)

Expand All @@ -34,7 +34,7 @@

External embeds (like YouTube, Google Maps, and Twitter/X posts) and script tags (like GA4 and GTM) are notorious for blocking the main thread, lowering Lighthouse performance scores, and causing layout shifts. This plugin solves that by providing:

- ⚡ **React Compiler Pre-Optimized**: Ships fully compiled with automated auto-memoization at build-time. Zero unnecessary re-renders.
- ⚡ **React 18 & 19 Compatible**: Supports both React versions with the matching React DOM version.
- 🚀 **Non-Blocking Execution**: Lazy loading scripts using `requestIdleCallback` and fine-grained loading strategies.
- ⚡ **React 19 Resource Pre-initialization**: Leverages native `ReactDOM.preinit` and `ReactDOM.preload` during SSG/SSR.
- 🧠 **Smart Memory Caching**: Built-in script deduplication and stylesheet cache registry.
Expand Down Expand Up @@ -63,6 +63,8 @@ External embeds (like YouTube, Google Maps, and Twitter/X posts) and script tags

## 📦 Installation

Supports React 18 and React 19 with the matching React DOM version.

<a id="installation"></a>
Install via your preferred package manager:

Expand Down
6 changes: 3 additions & 3 deletions packages/rspress-plugin-third-parties/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ titleSuffix: 'Zero Main-Thread Blocking Third-Party Integrations'
hero:
name: Rspress Third Parties
text: High-Performance Third-Party Integrations
tagline: Zero main-thread blocking YouTube, Google Maps, Tweet/X Embeds, GA4, GTM & custom script strategies built for React 19 & React Compiler.
tagline: YouTube, Google Maps, Tweet/X Embeds, GA4, GTM & custom script loading strategies for React 18 & 19.
image:
src: /rspress-plugin-third-party-hero.png
alt: Rspress Third Parties Hero Image
Expand All @@ -22,8 +22,8 @@ features:
- title: '⚡ Zero Main-Thread Blocking'
details: Defers heavy third-party scripts via <code>requestIdleCallback</code> and lazy loading strategies to preserve 100/100 Lighthouse performance.
icon: 🚀
- title: '🧠 React Compiler Pre-Optimized'
details: Ships pre-compiled with <code>React 19</code> auto-memoization at build time for instant client-side rendering with zero runtime overhead.
- title: '🧠 React 18 & 19 Compatible'
details: Supports <code>React 18</code> and <code>React 19</code> with the matching React DOM version.
icon: ⚡
- title: '🗺️ Media, Tweets & Location Embeds'
details: High-performance, zero-layout-shift components for YouTube, Twitter/X Posts, Google Maps (Point Pedro!), GA4, and Google Tag Manager.
Expand Down
5 changes: 1 addition & 4 deletions packages/rspress-plugin-third-parties/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"provenance": true
},
"dependencies": {
"react-compiler-runtime": "0.0.0-experimental-a1856f3-20260507",
"react-tweet": "^3.3.1",
"third-party-capital": "^3.0.0"
},
Expand All @@ -66,11 +65,9 @@
"@rspress/core": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "^18.3.7",
"babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-render-to-markdown": "^18.3.1",
"rspress-plugin-third-parties": "workspace:*",
"typescript": "catalog:"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/rspress-plugin-third-parties/rstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ define.lib({
target: 'web',
autoExternal: true,
},
plugins: [pluginReact({ reactCompiler: { target: '18' } })],
plugins: [pluginReact()],
Comment thread
SoonIter marked this conversation as resolved.
Resolved
},
],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { requestIdleCallback, setAttributesFromProps } from './utils';
const ScriptCache = new Map<string, Promise<Event>>();
const LoadCache = new Set<string>();

// Deduplication tracker for React 18 stylesheets
// Deduplication tracker for stylesheets inserted without resource APIs
const insertedStylesheets = new Set<string>();

export interface ScriptProps extends Omit<
Expand Down Expand Up @@ -57,7 +57,7 @@ const insertStylesheets = (stylesheets: string[]) => {
return;
}

// Fall back to standard DOM manipulation on React 18 client-side
// Fall back to standard DOM manipulation when resource APIs are unavailable
if (typeof window !== 'undefined') {
const head = document.head;
stylesheets.forEach((stylesheet: string) => {
Expand Down
41 changes: 30 additions & 11 deletions packages/rspress-plugin-third-parties/tests/components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type PlaywrightFixture,
} from '../../../e2e/test.ts';
import { useRspressDevServer } from '../../../e2e/utils.ts';
import { reactDom } from '../src/components/reactDom';

const pageUrl = useRspressDevServer(join(import.meta.dirname, 'fixture'));
const scenarioUrl = (scenario: string) =>
Expand Down Expand Up @@ -189,15 +190,22 @@ describe('Script', () => {
});
}

test('preinitializes stylesheets with medium precedence', async ({
test('loads stylesheets with resource APIs or the DOM fallback', async ({
page,
}) => {
await page.goto(scenarioUrl('script-styles'));
await expect(
page.locator(
'link[rel="stylesheet"][href="https://example.com/style.css"]',
),
).toHaveAttribute('data-precedence', 'medium');
const stylesheet = page.locator(
'link[rel="stylesheet"][href="https://example.com/style.css"]',
);
await expect(stylesheet).toHaveCount(1);
if (
typeof reactDom.preinit === 'function' ||
typeof reactDom.experimental_preinit === 'function'
) {
await expect(stylesheet).toHaveAttribute('data-precedence', 'medium');
} else {
await expect(stylesheet).not.toHaveAttribute('data-precedence');
}
});

test('creates exactly one scroll button from the native load callback', async ({
Expand Down Expand Up @@ -265,14 +273,25 @@ describe('YouTubeEmbed', () => {
await expect(embed.locator('..')).toHaveCSS('height', '360px');
});

test('preconnects to the script CDN', async ({ page }) => {
test('preconnects to the script CDN when resource APIs are available', async ({
page,
}) => {
await page.goto(scenarioUrl('youtube-default'));
await expect(
page.locator('link[rel="preconnect"][href="https://cdn.jsdelivr.net"]'),
).toHaveAttribute('crossorigin', '');
await expect(page.locator('lite-youtube')).toHaveCount(1);
const preconnect = page.locator(
'link[rel="preconnect"][href="https://cdn.jsdelivr.net"]',
);
if (
typeof reactDom.preconnect === 'function' ||
typeof reactDom.experimental_preconnect === 'function'
) {
await expect(preconnect).toHaveAttribute('crossorigin', '');
} else {
await expect(preconnect).toHaveCount(0);
}
});

test('loads with the React 18 resource API fallback', async ({ page }) => {
test('loads when resource APIs are unavailable', async ({ page }) => {
await page.goto(scenarioUrl('youtube-no-resources'));
await expect(page.locator('lite-youtube')).toHaveAttribute(
'videoid',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ export function Scenarios(): JSX.Element {
useEffect(() => {
const name = new URLSearchParams(location.search).get('scenario') || '';
if (name === 'youtube-no-resources') {
// Exercise the React 18 fallback without resource APIs. This page has its
// Exercise the fallback without resource APIs. This page has its
// own browser context, so the override cannot leak into other tests.
reactDom.preconnect = undefined;
Reflect.set(reactDom, 'preconnect', undefined);
reactDom.experimental_preconnect = undefined;
reactDom.preinit = undefined;
Reflect.set(reactDom, 'preinit', undefined);
reactDom.experimental_preinit = undefined;
reactDom.preload = undefined;
Reflect.set(reactDom, 'preload', undefined);
reactDom.experimental_preload = undefined;
}
setScenario(name);
Expand Down
Loading
Loading