From cff16228ac8ea69f60deb12c299b84490a75f9d8 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 6 Sep 2026 17:14:22 -0600 Subject: [PATCH] Preserve host React hook and prepare 0.90.0 --- .github/workflows/typescript-check.yml | 3 ++ CHANGELOG.md | 7 +++ package-lock.json | 4 +- package.json | 7 +-- src/util/runExtension.ts | 4 -- tests/react-host.test.cjs | 59 ++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 tests/react-host.test.cjs diff --git a/.github/workflows/typescript-check.yml b/.github/workflows/typescript-check.yml index 6ed303b..f36ef92 100644 --- a/.github/workflows/typescript-check.yml +++ b/.github/workflows/typescript-check.yml @@ -23,3 +23,6 @@ jobs: - name: Run TypeScript compilation run: npx tsc --noEmit + + - name: Run unit tests + run: npm run test:unit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0d836ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 0.90.0 + +- Preserve Roam's built-in React 18 external-store hook when extensions initialize. +- Remove React 17 shim injection from the package build configuration. Extensions using this configuration now require a host that provides `useSyncExternalStore`, such as Roam's documented React 18.2.0. +- Existing extension bundles must be rebuilt and republished to adopt this change. Custom build configurations that inject a shim must be updated separately. diff --git a/package-lock.json b/package-lock.json index a14f0c1..21c5e46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roamjs-components", - "version": "0.89.0", + "version": "0.90.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "roamjs-components", - "version": "0.89.0", + "version": "0.90.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f4507d6..40d631f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "roamjs-components", "description": "Expansive toolset, utilities, & components for developing RoamJS extensions.", - "version": "0.89.0", + "version": "0.90.0", "main": "index.js", "types": "index.d.ts", "scripts": { @@ -12,7 +12,8 @@ "preversion": "npm run lint", "version": "npm run format && git add -A src", "test": "playwright test", - "start": "samepage dev" + "start": "samepage dev", + "test:unit": "node --test tests/react-host.test.cjs" }, "sideEffects": false, "license": "MIT", @@ -102,7 +103,7 @@ "marked=window.RoamLazy.Marked", "marked-react=window.RoamLazy.MarkedReact", "nanoid=window.Nanoid;module.exports.nanoid=window.Nanoid", - "react=window.React;module.exports.useSyncExternalStore=require(\"use-sync-external-store/shim\").useSyncExternalStore", + "react=window.React", "react/jsx-runtime=./node_modules/react/jsx-runtime.js", "react-dom=window.ReactDOM", "react-youtube=window.ReactYoutube", diff --git a/src/util/runExtension.ts b/src/util/runExtension.ts index bb2592c..4105fc6 100644 --- a/src/util/runExtension.ts +++ b/src/util/runExtension.ts @@ -7,7 +7,6 @@ import { getRoamJSVersionEnv, } from "./env"; import type { Registry } from "../types"; -import { useSyncExternalStore } from "use-sync-external-store/shim"; import { provideExtensionApi } from "./extensionApiContext"; import apiPost from "./apiPost"; import renderToast from "../components/Toast"; @@ -36,9 +35,6 @@ const renderLoadFailure = ({ const runExtension = ( run: RunExtension ): { onload: (args: OnloadArgs) => void; onunload: () => void } => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore React17 shim - window.React.useSyncExternalStore = useSyncExternalStore; let unload: (() => void) | undefined; const extensionId = getRoamJSExtensionIdEnv(); const registry: Registry = { diff --git a/tests/react-host.test.cjs b/tests/react-host.test.cjs new file mode 100644 index 0000000..0125c83 --- /dev/null +++ b/tests/react-host.test.cjs @@ -0,0 +1,59 @@ +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const vm = require("node:vm"); +const ts = require("typescript"); + +test("initializing and unloading extensions preserves the host React hook", () => { + const nativeHook = () => 42; + const react = Object.freeze({ useSyncExternalStore: nativeHook }); + const window = { React: react }; + const body = { addEventListener() {}, removeEventListener() {} }; + const module = { exports: {} }; + const source = ts.transpileModule( + fs.readFileSync("src/util/runExtension.ts", "utf8"), + { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + }, + } + ).outputText; + vm.runInNewContext(source, { + module, + exports: module.exports, + window, + document: { body, getElementById: () => null }, + require: (name) => { + if (name === "./env") return { getRoamJSExtensionIdEnv: () => "test" }; + if (name === "use-sync-external-store/shim") { + throw new Error( + "Extension initialization must not load the React 17 shim" + ); + } + return {}; + }, + }); + const first = module.exports.default(async () => {}); + const second = module.exports.default(async () => {}); + assert.equal(window.React, react); + assert.equal(window.React.useSyncExternalStore, nativeHook); + first.onunload(); + second.onunload(); + assert.equal(window.React.useSyncExternalStore, nativeHook); +}); + +test("the configured React external exports the host without modifying it", () => { + const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); + const mapping = pkg.samepage.external.find((entry) => + entry.startsWith("react=") + ); + const react = Object.freeze({ useSyncExternalStore: () => 42 }); + const module = { exports: {} }; + vm.runInNewContext(`module.exports = ${mapping.slice("react=".length)};`, { + module, + window: { React: react }, + }); + assert.equal(module.exports, react); + assert.equal(module.exports.useSyncExternalStore(), 42); +});