Skip to content

Commit

Permalink
feat: Initial support for type safety <react.Component> instead of <r…
Browse files Browse the repository at this point in the history
…eact:Component>

Related to #1 and #32
  • Loading branch information
bfanger committed Oct 16, 2024
1 parent 85c73dd commit 548bfe6
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 248 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
]
},
"devDependencies": {
"@playwright/test": "^1.48.0",
"@playwright/test": "^1.48.1",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.7.0",
"@sveltejs/kit": "^2.7.1",
"@sveltejs/package": "^2.3.5",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@testing-library/react": "^16.0.1",
Expand All @@ -70,15 +70,15 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-youtube": "^10.1.0",
"svelte": "5.0.0-next.262",
"svelte": "5.0.0-next.267",
"svelte-check": "^4.0.5",
"svelte-youtube-lite": "^0.5.1",
"svelte2tsx": "^0.7.22",
"typescript": "^5.6.3",
"typescript-eslint": "^8.8.1",
"vite": "^5.4.8",
"typescript-eslint": "^8.9.0",
"vite": "^5.4.9",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.1.2"
"vitest": "^2.1.3"
},
"dependencies": {
"magic-string": "^0.30.12"
Expand Down
468 changes: 234 additions & 234 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/demo/react-components/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import * as React from "react";

import $ from "./Alert.module.css";

type Props = {
type AlertProps = {
type?: "primary";
children?: React.ReactNode;
};
const Tooltip: React.FC<Props> = ({ type = "primary", children }) => {
const Tooltip: React.FC<AlertProps> = ({ type = "primary", children }) => {
const classNames = [$.alert];
if (type === "primary") {
classNames.push($["alert-primary"]);
Expand Down
4 changes: 2 additions & 2 deletions src/demo/react-components/Counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { reactify } from "svelte-preprocess-react";

const Button = reactify(ButtonSvelte);

type Props = {
type CounterProps = {
initial?: number;
onCount?: (count: number) => void;
};
const Counter: React.FC<Props> = ({ initial = 0, onCount }) => {
const Counter: React.FC<CounterProps> = ({ initial = 0, onCount }) => {
const [count, setCount] = React.useState(initial);

function decrease() {
Expand Down
14 changes: 13 additions & 1 deletion src/lib/internal/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComponentClass, FunctionComponent, ReactNode } from "react";
import type { Snippet } from "svelte";
import type { Component, Snippet } from "svelte";

export type HandlerName<T extends string> = `on${Capitalize<T>}`;
export type EventName<T extends string> = T extends `on${infer N}`
Expand Down Expand Up @@ -65,3 +65,15 @@ export type SvelteInit = {
hooks: { Hook: FunctionComponent; key: number }[];
parent?: TreeNode;
};

export type ChildrenPropsAsSnippet<T> = T extends {
children: React.ReactNode;
}
? Omit<T, "children"> & { children: Snippet }
: T extends { children?: React.ReactNode }
? Omit<T, "children"> & { children?: Snippet }
: T;

export type Sveltified<T extends React.JSXElementConstructor<any>> = Component<
ChildrenPropsAsSnippet<React.ComponentProps<T>>
>;
73 changes: 70 additions & 3 deletions src/lib/sveltify.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,66 @@
import type ReactDOMServer from "react-dom/server";
import * as React from "react";
import type { SvelteInit, TreeNode } from "./internal/types.js";
import type {
ChildrenPropsAsSnippet,
SvelteInit,
Sveltified,
TreeNode,
} from "./internal/types.js";
import Bridge, { type BridgeProps } from "./internal/Bridge.svelte.js";
import ReactWrapper from "./internal/ReactWrapper.svelte";
import { setPayload } from "./reactify.js";
import type { Component } from "svelte";

let sharedRoot: TreeNode | undefined;

export default function sveltify<
T extends {
[key: string]: React.JSXElementConstructor<any>;
},
>(
components: T,
createPortal: BridgeProps["createPortal"],
ReactDOMClient: any,
renderToString?: typeof ReactDOMServer.renderToString,
): {
[K in keyof T]: Sveltified<T[K]>;
};
/**
* Convert a React component into a Svelte component.
*/
export default function sveltify<P>(
reactComponent: React.FunctionComponent<P> | React.ComponentClass<P>,
export default function sveltify<T extends React.JSXElementConstructor<any>>(
components: T,
createPortal: BridgeProps["createPortal"],
ReactDOMClient: any,
renderToString?: typeof ReactDOMServer.renderToString,
): Sveltified<T>;
export default function sveltify(
components: any,
createPortal: BridgeProps["createPortal"],
ReactDOMClient: any,
renderToString?: typeof ReactDOMServer.renderToString,
): any {
if (
typeof components === "function" ||
("render" in components && typeof components.render === "function") ||
"_context" in components // a Context.Provider
) {
return single(
components as React.FC,
createPortal,
ReactDOMClient,
renderToString,
);
}
return multiple(components, createPortal, ReactDOMClient, renderToString);
}

function single<T extends React.FC | React.ComponentClass>(
reactComponent: T,
createPortal: BridgeProps["createPortal"],
ReactDOMClient: any,
renderToString?: typeof ReactDOMServer.renderToString,
): Component<ChildrenPropsAsSnippet<React.ComponentProps<T>>> {
if (
typeof reactComponent !== "function" &&
typeof reactComponent === "object" &&
Expand Down Expand Up @@ -203,3 +248,25 @@ function inject(open: string, close: string, content: string, target: string) {
target.substring(0, start + open.length) + content + target.substring(end)
);
}

function multiple<
T extends {
[key: string]: React.FC | React.ComponentClass;
},
>(
reactComponents: T,
createPortal: BridgeProps["createPortal"],
ReactDOMClient: any,
renderToString?: typeof ReactDOMServer.renderToString,
): {
[K in keyof T]: Component<ChildrenPropsAsSnippet<React.ComponentProps<T[K]>>>;
} {
return Object.fromEntries(
Object.entries(reactComponents).map(([key, reactComponent]) => {
return [
key,
single(reactComponent, createPortal, ReactDOMClient, renderToString),
];
}),
) as any;
}
24 changes: 24 additions & 0 deletions src/routes/typesafe/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import Counter from "../../demo/react-components/Counter";
import Alert from "../../demo/react-components/Alert";
import { sveltify } from "svelte-preprocess-react";
import ReactDOM from "react-dom/client";
import { createPortal } from "react-dom";
const react = sveltify({ Counter, Alert }, createPortal, ReactDOM);
let count = $state(1);
</script>

<h1>{count}</h1>

<react.Counter
initial={count}
onCount={(val) => {
count = val;
}}
/>

<react.Alert type="primary">
The count is {count}
</react.Alert>

0 comments on commit 548bfe6

Please sign in to comment.