From d920b12d1b7fa382ffaedcc851386a05894f9b87 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 10:09:21 +0000 Subject: [PATCH] Remove transitional MessageChannel shim from nodePolyfills Since @enonic/react4xp 7.0.0 resolves React packages with the edge-light condition (enonic/npm-react4xp#1627), SSR bundles load react-dom/server.edge, which does not construct a MessageChannel at module load. The shim only existed to let server.browser bundles load, so it can go, along with the server.browser test bundle and the test that pinned the shim's contract. Apps must build with @enonic/react4xp >= 7.0.0; older bundles resolving react-dom/server.browser will now fail to load at SSR engine warmup. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FxjAJqqTs6SpY2ze8nZJYm --- build.gradle | 11 +------ package.json | 1 - .../enonic/polyfill-react4xp/nodePolyfills.ts | 29 ------------------ .../react4xp/ssr/renderer/RendererTest.java | 30 ------------------- .../js/react-bundle-server-browser-entry.js | 15 ---------- 5 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 src/test/js/react-bundle-server-browser-entry.js diff --git a/build.gradle b/build.gradle index 97789118b..6fbeb433b 100644 --- a/build.gradle +++ b/build.gradle @@ -121,17 +121,8 @@ tasks.register('bundleReactForTest', NpmTask) { environment = ['FORCE_COLOR': 'true'] } -tasks.register('bundleReactServerBrowserForTest', NpmTask) { - dependsOn npmInstall - args = ['run', 'build:react-server-browser-test-bundle'] - inputs.file('src/test/js/react-bundle-server-browser-entry.js') - inputs.file('package-lock.json') - outputs.file('build/resources/test/test-fixtures/react-bundle-server-browser.js') - environment = ['FORCE_COLOR': 'true'] -} - test { - dependsOn bundleReactForTest, bundleReactServerBrowserForTest + dependsOn bundleReactForTest useJUnitPlatform { } } diff --git a/package.json b/package.json index 9a18b194b..8df8c799c 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,6 @@ "postcov": "npm run lcov", "reinstall": "npm run clean && npm run local", "build:react-test-bundle": "esbuild src/test/js/react-bundle-entry.js --bundle --format=iife --platform=neutral --define:process.env.NODE_ENV=\\\"production\\\" --outfile=build/resources/test/test-fixtures/react-bundle.js", - "build:react-server-browser-test-bundle": "esbuild src/test/js/react-bundle-server-browser-entry.js --bundle --format=iife --platform=neutral --define:process.env.NODE_ENV=\\\"production\\\" --outfile=build/resources/test/test-fixtures/react-bundle-server-browser.js", "rollup:nodePolyfills": "npx rollup --config rollup.config.nodePolyfills.mjs", "test": "npm run test:jest && npm run test:deno", "test:deno": "deno test --coverage=build/deno/coverage --env-file=src/deno/.env --importmap=src/deno/importmap.json --no-check src/deno/**/*.test.ts", diff --git a/src/main/resources/lib/enonic/polyfill-react4xp/nodePolyfills.ts b/src/main/resources/lib/enonic/polyfill-react4xp/nodePolyfills.ts index fc0fe4c23..c82c3a769 100644 --- a/src/main/resources/lib/enonic/polyfill-react4xp/nodePolyfills.ts +++ b/src/main/resources/lib/enonic/polyfill-react4xp/nodePolyfills.ts @@ -9,23 +9,6 @@ import {TextEncoder} from '@sinonjs/text-encoding'; import 'core-js/modules/web.atob.js'; import 'core-js/modules/web.btoa.js'; -// Transitional MessageChannel shim: react-dom/server.browser constructs a -// MessageChannel at module-load time (unconditionally, before any render call) for -// its streaming scheduler. With this minimal polyfill, the module loads and -// renderToString — which doesn't touch the channel — works. -// -// LIMITATIONS: postMessage is intentionally a no-op. The streaming APIs -// (renderToReadableStream, prerender) WILL silently hang because the no-op timers -// can never deliver messages. For streaming, consumers must use -// react-dom/server.edge (which has no MessageChannel dependency). -// -// Remove this once @enonic/react4xp's webpack config aliases react-dom/server to -// react-dom/server.edge — see https://github.com/facebook/react/issues/31827 -function MessagePortShim(this: { onmessage: ((ev: { data: unknown }) => void) | null; postMessage: (data: unknown) => void }) { - this.onmessage = null; - this.postMessage = function () { /* no-op: SSR has no async */ }; -} - (function (context) { //────────────────────────────────────────────────────────────────────────── // Timers: When doing SSR it makes no sense to do anything asyncronously. @@ -38,17 +21,5 @@ function MessagePortShim(this: { onmessage: ((ev: { data: unknown }) => void) | if (typeof context['TextEncoder'] === 'undefined') { context['TextEncoder'] = TextEncoder; } - - //────────────────────────────────────────────────────────────────────────── - // MessageChannel: see comment above MessagePortShim. - //────────────────────────────────────────────────────────────────────────── - if (typeof context['MessageChannel'] === 'undefined') { - context['MessageChannel'] = function MessageChannel(this: { port1: object; port2: object }) { - // @ts-expect-error TS2350: 'new' expression with non-constructor type. - this.port1 = new MessagePortShim(); - // @ts-expect-error TS2350: 'new' expression with non-constructor type. - this.port2 = new MessagePortShim(); - }; - } //@ts-expect-error TS2695: Left side of comma operator is unused and has no side effects. })((1, eval)('this') as Partial); diff --git a/src/test/java/com/enonic/lib/react4xp/ssr/renderer/RendererTest.java b/src/test/java/com/enonic/lib/react4xp/ssr/renderer/RendererTest.java index c310c87e4..c86e1a0fb 100644 --- a/src/test/java/com/enonic/lib/react4xp/ssr/renderer/RendererTest.java +++ b/src/test/java/com/enonic/lib/react4xp/ssr/renderer/RendererTest.java @@ -144,36 +144,6 @@ void render_real_react_component_via_server_edge() } } - @Test - void render_real_react_component_via_server_browser_with_message_channel_shim() - { - // server.browser constructs `new MessageChannel()` at module-load time. This test - // pins the contract that the transitional shim in nodePolyfills.ts lets the module - // load and that renderToString (via server-legacy.browser) produces HTML. - final String reactBundle = readClasspathResource( "/test-fixtures/react-bundle-server-browser.js" ); - - final Map fixtures = new HashMap<>(); - fixtures.put( POLYFILL_BASICS, readClasspathResource( POLYFILL_BASICS ) ); - fixtures.put( POLYFILL_NODE, readClasspathResource( POLYFILL_NODE ) ); - fixtures.put( CHUNKS_GLOBALS_JSON, "{ \"main\": { \"js\": \"globals.js\" } }" ); - fixtures.put( GLOBALS_JS, - reactBundle + "\n" + "globalThis.React4xp = {" + " '_components/Greeter': {" + " default: function(props) {" + - " return React.createElement('h1', null, 'Hello, ' + props.name + '!');" + " }" + " }" + "};" ); - - final Renderer renderer = new Renderer( strictReader( fixtures ), testConfig(), 11L, engine, freshSource() ); - try - { - final Map result = renderer.render( "_components/Greeter", "{\"name\":\"World\"}", new String[0] ); - - assertNull( result.get( KEY_ERROR ), "expected success, got error: " + result.get( KEY_ERROR ) ); - assertEquals( "

Hello, World!

", result.get( KEY_HTML ) ); - } - finally - { - renderer.close(); - } - } - @Test void render_unknown_entry_propagates_illegal_state() { diff --git a/src/test/js/react-bundle-server-browser-entry.js b/src/test/js/react-bundle-server-browser-entry.js deleted file mode 100644 index b315196df..000000000 --- a/src/test/js/react-bundle-server-browser-entry.js +++ /dev/null @@ -1,15 +0,0 @@ -// Bundles real React 19 + react-dom/server.browser for the GraalJS integration test. -// server.browser is what @enonic/react4xp's webpack config currently resolves to (default -// 'browser' condition); see https://github.com/facebook/react/issues/31827. -// -// At module-load time, server.browser does `new MessageChannel()` for its streaming -// scheduler. Without the MessageChannel polyfill in nodePolyfills.ts, this would throw -// ReferenceError before any render call. With the polyfill, the module loads and -// `renderToString` (which goes through server-legacy.browser and never touches the -// channel) works. Streaming APIs would silently hang — they're not exercised here. - -const React = require('react'); -const ReactDOMServer = require('react-dom/server.browser'); - -globalThis.React = React; -globalThis.ReactDOMServer = { renderToString: ReactDOMServer.renderToString };