Skip to content

Commit

Permalink
chore: Improved typing
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Fanger committed Jun 17, 2022
1 parent d3626b9 commit 24e7c17
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.eslint.json",
"extraFileExtensions": [".cjs", ".svelte"]
"extraFileExtensions": [".svelte"]
}
}
3 changes: 3 additions & 0 deletions src/demo/react-components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* A react component using a Svelte Button component
*/
import * as React from "react";
import reactifySvelte from "../../lib/reactifySvelte";
import ButtonSvelte from "../components/Button.svelte";
Expand Down
12 changes: 4 additions & 8 deletions src/lib/internal/SvelteWrapper.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<script lang="ts">
import {
createEventDispatcher,
onMount,
tick,
type SvelteComponent as SvelteComponentType,
} from "svelte";
import { createEventDispatcher, onMount, tick } from "svelte";
import type { SvelteConstructor } from "./types";
const dispatch = createEventDispatcher();
export let SvelteComponent: typeof SvelteComponentType;
export let SvelteComponent: SvelteConstructor;
export let props: Record<string, any>;
export let events: Record<string, any>;
export let slot: HTMLElement | undefined = undefined;
let slot: HTMLElement | undefined = undefined;
let instance: any;
$: instance && syncEvents(events);
Expand Down
41 changes: 34 additions & 7 deletions src/lib/internal/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
export interface ConstructorOf<T> {
export type ConstructorOf<T> = {
new (): T;
}
};

export type SvelteConstructor<Props = any, Events = any, Slot = any> = {
name: string;
prototype: {
$$prop_def: Props;
$$events_def: Events;
$$slot_def: Slot;
};
};

export type HandlerName<T extends string> = `on${Capitalize<T>}`;
export type EventName<T extends string> = T extends `on${infer N}`
? Uncapitalize<N>
: never;

export type SvelteEventHandlers<T> = T extends Record<
infer Key extends string,
infer Value
>
? Partial<Record<HandlerName<Key>, (e: Value) => void | boolean>>
: never;

type Uppercase =
| "A"
Expand All @@ -26,9 +47,15 @@ type Uppercase =
| "Y"
| "Z";

type EventKey = `on${Uppercase}${string}`;
type ExcludeProps<T> = T extends EventKey ? T : never;
type ExcludeEvents<T> = T extends EventKey ? never : T;
type ReactEventProp = `on${Uppercase}${string}`;
type ExcludeProps<T> = T extends ReactEventProp ? T : never;
type ExcludeEvents<T> = T extends ReactEventProp ? never : T;

export type PropsOf<T> = Omit<T, ExcludeProps<keyof T>>;
export type EventsOf<T> = Omit<T, ExcludeEvents<keyof T>>;
export type EventProps<ReactProps> = Omit<
ReactProps,
ExcludeEvents<keyof ReactProps>
>;
export type OmitEventProps<ReactProps> = Omit<
ReactProps,
ExcludeProps<keyof ReactProps>
>;
27 changes: 10 additions & 17 deletions src/lib/reactifySvelte.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import {
createElement,
useEffect,
useRef,
type FunctionComponent,
} from "react";
import type {
SvelteComponent as SvelteComponentType,
SvelteComponentTyped,
} from "svelte";
import type { ConstructorOf } from "./internal/types";
import { createElement, useEffect, useRef } from "react";
import type { FunctionComponent } from "react";
import SvelteWrapper from "./internal/SvelteWrapper.svelte";
import type { SvelteConstructor, SvelteEventHandlers } from "./internal/types";

export default function reactifySvelte<P>(
SvelteComponent: ConstructorOf<SvelteComponentTyped<P>>
): FunctionComponent<P> {
export default function reactifySvelte<P = any, E = any>(
SvelteComponent: SvelteConstructor<P, E>
): FunctionComponent<P & SvelteEventHandlers<E>> {
const { name } = SvelteComponent as any;
const named = {
[SvelteComponent.name](options: any) {
[name](options: any) {
const props = extractProps(options);
const events = extractListeners(options);

const wrapperRef = useRef<HTMLElement>();
const svelteRef = useRef<SvelteComponentType>();
const svelteRef = useRef<SvelteWrapper>();
const slotRef = useRef<HTMLElement>();
const childrenRef = useRef<HTMLElement>();

Expand Down Expand Up @@ -89,7 +82,7 @@ export default function reactifySvelte<P>(
);
},
};
return named[SvelteComponent.name];
return named[name];
}

function extractProps(options: Record<string, any>): Record<string, any> {
Expand Down
2 changes: 0 additions & 2 deletions src/tests/__snapshots__/preprocess.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Vitest Snapshot v1

exports[`svelte-preprocess-react > should fail on bindings 1`] = `"'count' is not a valid binding"`;

exports[`svelte-preprocess-react > should inject a script tag 1`] = `
"<script>
import sveltifyReact from \\"svelte-preprocess-react/sveltifyReact18\\";
Expand Down
13 changes: 13 additions & 0 deletions src/tests/fixtures/Dog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
const dispatch = createEventDispatcher();
export let name: string;
onMount(() => {
dispatch("bark", "woof");
});
</script>

<svelte-dog on:click>{name}</svelte-dog>
4 changes: 3 additions & 1 deletion src/tests/preprocess.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ describe("svelte-preprocess-react", () => {
await preprocess(src, preprocessReact(), { filename });
failed = false;
} catch (err: any) {
expect(err.message).toMatchSnapshot();
expect(err.message).toMatchInlineSnapshot(
"\"'count' is not a valid binding\""
);
failed = true;
}
expect(failed).toBe(true);
Expand Down
17 changes: 17 additions & 0 deletions src/tests/reactifySvelte.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import * as React from "react";
import { renderToString } from "react-dom/server";
import reactifySvelte from "../lib/reactifySvelte";
import DogSvelte from "./fixtures/Dog.svelte";

describe("reactifySvelte", () => {
const Dog = reactifySvelte(DogSvelte);
type ReactProps = React.ComponentProps<typeof Dog>;
it("renders a svelte-wrapper", () => {
const props: ReactProps = { name: "Fido", onBark() {} };
const html = renderToString(React.createElement(Dog, props));
expect(html).toMatchInlineSnapshot(
'"<svelte-wrapper style=\\"display:contents\\"></svelte-wrapper>"'
);
});
});
17 changes: 17 additions & 0 deletions src/tests/reactifySvelte.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import * as React from "react";
import { renderToString } from "react-dom/server";
import reactifySvelte from "../lib/reactifySvelte";
import DogSvelte from "./fixtures/Dog.svelte";

describe("reactifySvelte", () => {
const Dog = reactifySvelte(DogSvelte);
type ReactProps = React.ComponentProps<typeof Dog>;
it("renders a svelte-wrapper", () => {
const props: ReactProps = { name: "Fido", onBark() {} };
const html = renderToString(<Dog/>);
expect(html).toMatchInlineSnapshot(
'"<svelte-wrapper style=\\"display:contents\\"></svelte-wrapper>"'
);
});
});
46 changes: 46 additions & 0 deletions src/tests/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { noop } from "svelte/internal";
import { describe, it } from "vitest";
import type {
EventName,
EventProps,
HandlerName,
OmitEventProps,
SvelteEventHandlers,
} from "../lib/internal/types";

const fn = noop as any;

describe("types", () => {
it("bogus test", () => {
// Types don't fail at runtime,
// Use `tsc --noEmit` to verify that the types are correct.

type ReactProps = { label: string; onClick(): void };

const testEventProps = fn as (_: EventProps<ReactProps>) => void;
testEventProps({ onClick() {} });

const testPropsOmitEventProps = fn as (
_: OmitEventProps<ReactProps>
) => void;
testPropsOmitEventProps({ label: "test" });

const testHandlerName = fn as (_: HandlerName<"click">) => void;
testHandlerName("onClick");

const testEventName = fn as (_: EventName<"onClick">) => void;
testEventName("click");

type SvelteEvents = {
click: MouseEvent;
};
const testSvelteEventHandlers = fn as (
_: SvelteEventHandlers<SvelteEvents>
) => void;
testSvelteEventHandlers({
onClick(event: MouseEvent) {
console.info(event);
},
});
});
});
1 change: 1 addition & 0 deletions tsconfig.eslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"src/**/*.d.ts",
"src/**/*.svelte",
// Additional extensions for eslint
"**/*.svelte",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
Expand Down

0 comments on commit 24e7c17

Please sign in to comment.