diff --git a/src/lib/internal/types.ts b/src/lib/internal/types.ts index 59d9f83..8109825 100644 --- a/src/lib/internal/types.ts +++ b/src/lib/internal/types.ts @@ -103,14 +103,14 @@ export type StaticPropComponents = { }; export type ReactDependencies = { - ReactDOM?: + ReactDOM: | { createRoot: (container: Element) => Root; // React 18 and above } | { render(component: React.ReactNode, container: Element): void; // React 17 and below }; - createPortal?: ( + createPortal: ( children: React.ReactNode, container: Element | DocumentFragment, key?: null | string, diff --git a/src/lib/preprocessReact.js b/src/lib/preprocessReact.js index 34e7b1d..b64cd00 100644 --- a/src/lib/preprocessReact.js +++ b/src/lib/preprocessReact.js @@ -85,18 +85,18 @@ function transform(content, options) { `import ${prefix}ReactDOM from "react-dom/client";`, `import { createPortal as ${prefix}createPortal} from "react-dom";`, ); - portal = `${prefix}createPortal`; + portal = `createPortal: ${prefix}createPortal`; } else { imports.push(`import ${prefix}ReactDOM from "react-dom";`); - portal = `${prefix}createPortal: ${prefix}ReactDOM.createPortal`; + portal = `createPortal: ${prefix}ReactDOM.createPortal`; } - const deps = [portal, `${prefix}ReactDOM`]; + const deps = [portal, `ReactDOM: ${prefix}ReactDOM`]; if (options.ssr) { imports.push( `import { renderToString as ${prefix}renderToString } from "react-dom/server";`, ); - deps.push(`${prefix}renderToString`); + deps.push(`renderToString: ${prefix}renderToString`); } const ast = parse(content, { diff --git a/src/lib/sveltify.svelte.ts b/src/lib/sveltify.svelte.ts index 6654e85..0f15095 100644 --- a/src/lib/sveltify.svelte.ts +++ b/src/lib/sveltify.svelte.ts @@ -37,6 +37,11 @@ function sveltify< T extends React.FC | React.ComponentClass | React.JSXElementConstructor, >(components: T, dependencies?: ReactDependencies): Sveltified; function sveltify(components: any, dependencies?: ReactDependencies): any { + if (!dependencies) { + throw new Error( + "{ createPortal, ReactDOM } are not injected, check svelte.config.js for: `preprocess: [preprocessReact()],`", + ); + } if ( typeof components !== "object" || // React.FC or JSXIntrinsicElements ("render" in components && typeof components.render === "function") || // React.ComponentClass @@ -47,27 +52,54 @@ function sveltify(components: any, dependencies?: ReactDependencies): any { return multiple(components, dependencies); } +type CacheEntry = ReactDependencies & { Sveltified: unknown }; +const cache = new WeakMap(); +const intrinsicElementCache: Record = {}; + function multiple< T extends { [key: string]: React.FC | React.ComponentClass; }, >( reactComponents: T, - dependencies?: ReactDependencies, + dependencies: ReactDependencies, ): { [K in keyof T]: Component>>; } { return Object.fromEntries( Object.entries(reactComponents).map(([key, reactComponent]) => { - return [key, single(reactComponent, dependencies)]; + const hit = + typeof reactComponent === "string" + ? intrinsicElementCache[reactComponent] + : cache.get(reactComponent); + if ( + hit && + hit.createPortal === dependencies.createPortal && + hit.ReactDOM === dependencies.ReactDOM && + hit.renderToString === dependencies.renderToString + ) { + return [key, hit.Sveltified]; + } + const entry = { + ...dependencies, + Sveltified: single(reactComponent, dependencies), + }; + if (typeof reactComponent === "string") { + intrinsicElementCache[reactComponent] = entry; + } else { + cache.set(reactComponent, entry); + } + return [key, entry.Sveltified]; }), ) as any; } function single( reactComponent: T, - dependencies: ReactDependencies = {}, + dependencies: ReactDependencies, ): Component>> { + const client = typeof document !== "undefined"; + const { createPortal, ReactDOM, renderToString } = dependencies; if ( typeof reactComponent !== "function" && typeof reactComponent === "object" && @@ -77,28 +109,9 @@ function single( // Fix SSR import issue where node doesn't import the esm version. 'react-youtube' reactComponent = (reactComponent as any).default; } - let { createPortal, ReactDOM, renderToString } = dependencies; - if ("inject$$createPortal" in dependencies) { - createPortal = - dependencies.inject$$createPortal as ReactDependencies["createPortal"]; - } - if ("inject$$ReactDOM" in dependencies) { - ReactDOM = dependencies.inject$$ReactDOM as ReactDependencies["ReactDOM"]; - } - if ("inject$$renderToString" in dependencies) { - renderToString = - dependencies.inject$$renderToString as ReactDependencies["renderToString"]; - } - - const client = typeof document !== "undefined"; function Sveltified(anchorOrPayload: any, $$props: any) { let standalone = !sharedRoot; - if (!createPortal || !ReactDOM) { - throw new Error( - "{ createPortal, ReactDOM } are not injected, check svelte.config.js for: `preprocess: [preprocessReact()],`", - ); - } $$props.svelteInit = (init: SvelteInit) => { let unroot: undefined | (() => void); @@ -210,6 +223,7 @@ function single( } } } + return Sveltified as any; } diff --git a/src/tests/__snapshots__/preprocess.spec.ts.snap b/src/tests/__snapshots__/preprocess.spec.ts.snap index fa5b81d..a555f2c 100644 --- a/src/tests/__snapshots__/preprocess.spec.ts.snap +++ b/src/tests/__snapshots__/preprocess.spec.ts.snap @@ -4,7 +4,7 @@ exports[`svelte-preprocess-react > should convert text content to react children " @@ -21,7 +21,7 @@ exports[`svelte-preprocess-react > should import 'react-dom/server' when ssr is " @@ -39,7 +39,7 @@ exports[`svelte-preprocess-react > should inject a script tag 1`] = ` " @@ -51,7 +51,7 @@ exports[`svelte-preprocess-react > should not import 'react-dom/server' when ssr " @@ -69,7 +69,7 @@ exports[`svelte-preprocess-react > should portal slotted content as children 1`] " @@ -80,7 +80,7 @@ exports[`svelte-preprocess-react > should process tags 1` " @@ -103,7 +103,7 @@ exports[`svelte-preprocess-react > should process tags }; let { context = createContext(false) }: Props = $props(); - let react = $derived(sveltify({ Provider: context.Provider }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString })); + let react = $derived(sveltify({ Provider: context.Provider }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString })); ; @@ -114,7 +114,7 @@ exports[`svelte-preprocess-react > should process tags 1`] = ` " @@ -133,7 +133,7 @@ exports[`svelte-preprocess-react > should process tags 2`] = ` //@ts-nocheck import Clicker from "./Clicker"; - const react = sveltify({ Clicker }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString }); + const react = sveltify({ Clicker }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString }); let count = 1; ; @@ -163,7 +163,7 @@ exports[`svelte-preprocess-react > should process (lowercase) ta " console.info("clicked")}> @@ -180,7 +180,7 @@ exports[`svelte-preprocess-react > should process {...rest} props 1`] = ` style: { backgroundColor: "#fcdef6" }, onClick: () => console.info("clicked"), }; -;const react = sveltify({ div: "div" }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString }); +;const react = sveltify({ div: "div" }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString }); Hi @@ -195,7 +195,7 @@ exports[`svelte-preprocess-react > should process {:else} {:then} and {:catch} s const number = 1; const Component: React.FC = () => null; - const react = sveltify({ Component }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString }); + const react = sveltify({ Component }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString }); ; @@ -228,7 +228,7 @@ exports[`svelte-preprocess-react > should process on:event forwarding 1`] = ` "