Skip to content

Commit

Permalink
feat: sveltify() caches Sveltified components
Browse files Browse the repository at this point in the history
  • Loading branch information
bfanger committed Nov 23, 2024
1 parent 297e965 commit 3b15a8b
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/lib/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/lib/preprocessReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
58 changes: 36 additions & 22 deletions src/lib/sveltify.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ function sveltify<
T extends React.FC | React.ComponentClass | React.JSXElementConstructor<any>,
>(components: T, dependencies?: ReactDependencies): Sveltified<T>;
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
Expand All @@ -47,27 +52,54 @@ function sveltify(components: any, dependencies?: ReactDependencies): any {
return multiple(components, dependencies);
}

type CacheEntry = ReactDependencies & { Sveltified: unknown };
const cache = new WeakMap<any, CacheEntry>();
const intrinsicElementCache: Record<string, CacheEntry> = {};

function multiple<
T extends {
[key: string]: React.FC | React.ComponentClass;
},
>(
reactComponents: T,
dependencies?: ReactDependencies,
dependencies: ReactDependencies,
): {
[K in keyof T]: Component<ChildrenPropsAsSnippet<React.ComponentProps<T[K]>>>;
} {
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<T extends React.FC | React.ComponentClass>(
reactComponent: T,
dependencies: ReactDependencies = {},
dependencies: ReactDependencies,
): Component<ChildrenPropsAsSnippet<React.ComponentProps<T>>> {
const client = typeof document !== "undefined";
const { createPortal, ReactDOM, renderToString } = dependencies;
if (
typeof reactComponent !== "function" &&
typeof reactComponent === "object" &&
Expand All @@ -77,28 +109,9 @@ function single<T extends React.FC | React.ComponentClass>(
// 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);
Expand Down Expand Up @@ -210,6 +223,7 @@ function single<T extends React.FC | React.ComponentClass>(
}
}
}

return Sveltified as any;
}

Expand Down
26 changes: 13 additions & 13 deletions src/tests/__snapshots__/preprocess.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`svelte-preprocess-react > should convert text content to react children
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
import Alert from "../../demo/react-components/Alert";
const react = sveltify({ Alert }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString });
const react = sveltify({ Alert }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });
const count = 0;
;</script>
Expand All @@ -21,7 +21,7 @@ exports[`svelte-preprocess-react > should import 'react-dom/server' when ssr is
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
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;
;</script>
Expand All @@ -39,7 +39,7 @@ exports[`svelte-preprocess-react > should inject a script tag 1`] = `
"<script>
import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
const react = sveltify({ Counter }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString });
const react = sveltify({ Counter }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });
</script>
<!-- Counter could be a global variable -->
Expand All @@ -51,7 +51,7 @@ exports[`svelte-preprocess-react > should not import 'react-dom/server' when ssr
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { sveltify } from "svelte-preprocess-react";
import Clicker from "./Clicker";
const react = sveltify({ Clicker }, { inject$$createPortal, inject$$ReactDOM });
const react = sveltify({ Clicker }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM });
let count = 1;
;</script>
Expand All @@ -69,7 +69,7 @@ exports[`svelte-preprocess-react > should portal slotted content as children 1`]
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
import Alert from "../../demo/react-components/Alert";
const react = sveltify({ Alert }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString });
const react = sveltify({ Alert }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });
;</script>
<react.Alert react$children="A simple primary alert. Check it out!" />
Expand All @@ -80,7 +80,7 @@ exports[`svelte-preprocess-react > should process <react.Component.Item> tags 1`
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
import List from "./List";
const react = sveltify({ List , inject$$List$Item: List.Item , inject$$List$Item$Icon: List.Item.Icon }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString });
const react = sveltify({ List , inject$$List$Item: List.Item , inject$$List$Item$Icon: List.Item.Icon }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });
;</script>
<react.List>
Expand All @@ -103,7 +103,7 @@ exports[`svelte-preprocess-react > should process <react:Context.Provider> 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 }));
;</script>
<react.Provider value={true} react$children="content" />
Expand All @@ -114,7 +114,7 @@ exports[`svelte-preprocess-react > should process <react:component> tags 1`] = `
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
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;
;</script>
Expand All @@ -133,7 +133,7 @@ exports[`svelte-preprocess-react > should process <react:component> 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;
;</script>
Expand Down Expand Up @@ -163,7 +163,7 @@ exports[`svelte-preprocess-react > should process <react:element> (lowercase) ta
"<script>
import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
const react = sveltify({ button: "button" }, { inject$$createPortal, inject$$ReactDOM, inject$$renderToString });
const react = sveltify({ button: "button" }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });
</script>
<react.button onClick={() => console.info("clicked")}>
Expand All @@ -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 });</script>
;const react = sveltify({ div: "div" }, { createPortal: inject$$createPortal, ReactDOM: inject$$ReactDOM, renderToString: inject$$renderToString });</script>
<react.div react$props={{ "aria-atomic": true, ...props, "aria-label": "after" }}>
<span>Hi</span>
Expand All @@ -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 });
;</script>
<!-- eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -->
Expand Down Expand Up @@ -228,7 +228,7 @@ exports[`svelte-preprocess-react > should process on:event forwarding 1`] = `
"<script lang="ts">import inject$$ReactDOM from "react-dom/client"; import { createPortal as inject$$createPortal} from "react-dom"; import { renderToString as inject$$renderToString } from "react-dom/server"; import { sveltify } from "svelte-preprocess-react";
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 { value = 0, onCount } = $props<{
value: number;
Expand Down

0 comments on commit 3b15a8b

Please sign in to comment.