Skip to content

Commit

Permalink
feat: Improved typing + error detection
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Fanger committed Jun 10, 2022
1 parent a0f70b3 commit 1123d07
Show file tree
Hide file tree
Showing 18 changed files with 210 additions and 117 deletions.
19 changes: 5 additions & 14 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,10 @@ export default {
};
```

or when used in combination with svelte-preprocess:

```js
// svelte.config.js
import preprocess from "svelte-preprocess";
import preprocessReact from "svelte-preprocess-react";

export default {
preprocess: [preprocess({ sourceMap: true }), preprocessReact()],
};
```

## Ideas / Roadmap

- Auto insert `<react:` for .tsx and .jsx imports.
- Research if it's possible to reliably determine if a Component is a React component at compile time.
- Improve Typescript support for events
- Add support for React 17 and below (autodetect version based on package.json)
- Auto insert `<react:` for .tsx and .jsx imports
- Add support for children
- Research if it's possible to reliably determine if a Component is a React component at compile time
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svelte-preprocess-react",
"version": "0.1.1",
"version": "0.2.0",
"license": "MIT",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -59,6 +59,8 @@
"postcss": "^8.4.14",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"sass": "^1.52.2",
"svelte": "^3.48.0",
"svelte-check": "^2.7.2",
Expand Down
9 changes: 6 additions & 3 deletions src/demo/react-components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import * as React from "react";

type Props = {
initialValue: number;
initial?: number;
onCount?: (count: number) => void;
};
const Counter: React.FC<Props> = ({ initialValue = 0 }) => {
const [count, setCount] = React.useState(initialValue);
const Counter: React.FC<Props> = ({ initial = 0, onCount }) => {
const [count, setCount] = React.useState(initial);
function decrease() {
setCount(count - 1);
onCount?.(count - 1);
}
function increase() {
setCount(count + 1);
onCount?.(count + 1);
}
return (
<>
Expand Down
35 changes: 25 additions & 10 deletions src/lib/svelte-preprocess-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
import type { PreprocessorGroup } from "svelte/types/compiler/preprocess";

export default function preprocessReact(): PreprocessorGroup {
// @todo Alternate import older React versions
// @todo Alternate import for older React versions
const importStatement =
'import sveltifyReact from "svelte-preprocess-react/sveltifyReact18";';

Expand All @@ -24,14 +24,19 @@ export default function preprocessReact(): PreprocessorGroup {
return { code: content };
}
const script = compiled.ast.instance || (compiled.ast.module as Script);
const offset = compiled.ast.html.start > script.start ? 0 : refs.offset;
const jsEnd = script.content.end + offset;
refs.components.forEach((component) => {
const code = `const React$${component} = sveltifyReact(${component});`;
s.appendRight(jsEnd, code);
});
const jsStart = script.content.start + offset;
s.appendRight(jsStart, importStatement);
const wrappers = refs.components
.map((component) => {
return `const React$${component} = sveltifyReact(${component});`;
})
.join(";");

if (!script) {
s.prepend(`<script>\n${importStatement}\n\n${wrappers}\n</script>\n\n`);
} else {
const offset = compiled.ast.html.start > script.start ? 0 : refs.offset;
s.appendRight(script.content.end + offset, wrappers);
s.appendRight(script.content.start + offset, importStatement);
}
return {
code: s.toString(),
map: s.generateMap(),
Expand All @@ -43,16 +48,26 @@ export default function preprocessReact(): PreprocessorGroup {
function replaceTags(node: TemplateNode, content: MagicString, refs: Refs) {
/* eslint-disable no-param-reassign */
if (node.type === "Element" && node.name.startsWith("react:")) {
if (node.children && node.children.length > 0) {
throw new Error(
"Nested components are not (yet) supported in svelte-preprocess-react"
);
}
const tag = node as Element;
const component = tag.name.slice(6);
const tagStart = node.start + refs.offset;
const tagEnd = node.end + refs.offset;
const closeStart = tagEnd - tag.name.length - 3;
content.overwrite(tagStart + 1, tagStart + 7, "React$");
if (content.slice(closeStart, closeStart + 8) === `</react:`) {
content.overwrite(closeStart + 2, closeStart + 8, `React$`);
}
if (refs.components.includes(component) === false) {
refs.components.push(component);
}
tag.attributes.forEach((attr) => {
if (attr.type === "EventHandler") {
const event = attr as Transition; // the BaseExpressionDirective is not exposed directly
const event = attr as Transition;
if (event.modifiers.length > 0) {
throw new Error(
"event modifier are not (yet) supported for React components"
Expand Down
6 changes: 4 additions & 2 deletions src/lib/sveltifyReact18.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ComponentClass, FunctionComponent } from "react";
import type { SvelteComponentTyped } from "svelte/internal";
import ReactWrapper from "./React18Wrapper.svelte";
import type { ConstructorOf } from "./types";

export default function sveltifyReact<P>(
export default function sveltifyReact18<P>(
ReactComponent: FunctionComponent<P> | ComponentClass<P>
): (props: P) => any {
): ConstructorOf<SvelteComponentTyped<P>> {
const ssr = typeof (ReactWrapper as any).$$render === "function";
if (ssr) {
const { $$render } = ReactWrapper as any;
Expand Down
34 changes: 34 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export interface ConstructorOf<T> {
new (): T;
}

type Uppercase =
| "A"
| "B"
| "C"
| "D"
| "E"
| "F"
| "G"
| "H"
| "I"
| "J"
| "K"
| "L"
| "M"
| "N"
| "S"
| "T"
| "U"
| "V"
| "W"
| "X"
| "Y"
| "Z";

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

export type PropsOf<T> = Omit<T, ExcludeProps<keyof T>>;
export type EventsOf<T> = Omit<T, ExcludeEvents<keyof T>>;
36 changes: 4 additions & 32 deletions src/routes/index.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,4 @@
<script lang="ts">
import Counter from "../demo/react-components/Counter";
import sveltify from "$lib/sveltifyReact18";
let toggle = true;
let initialValue = 10;
const ReactCounter = sveltify(Counter);
</script>

<label>
<input type="checkbox" bind:checked={toggle} />
{toggle}
</label>
{#if toggle}
<ReactCounter {initialValue} />
{/if}

<div>
<button
on:click={() => {
initialValue = Math.random();
}}>Change</button
>
</div>

<style>
label {
display: block;
user-select: none;
cursor: pointer;
}
</style>
<ul>
<li><a href="/via-preprocessor">via preprocessor</a></li>
<li><a href="/via-lib">via lib</a></li>
</ul>
9 changes: 9 additions & 0 deletions src/routes/via-lib.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import sveltifyReact18 from "$lib/sveltifyReact18";
import Counter from "../demo/react-components/Counter";
const ReactCounter = sveltifyReact18(Counter);
</script>

<ReactCounter count={10} onCount={console.info} />
5 changes: 5 additions & 0 deletions src/routes/via-preprocessor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
import Counter from "../demo/react-components/Counter";
</script>

<react:Counter initial={10} on:count={console.info} />
15 changes: 0 additions & 15 deletions src/tests/__snapshots__/preprocess-react.spec.ts.snap

This file was deleted.

16 changes: 16 additions & 0 deletions src/tests/__snapshots__/svelte-preprocess-react.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
// Vitest Snapshot v1

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

exports[`preprocess-react > should inject a script tag 1`] = `
"<script>
import sveltifyReact from \\"svelte-preprocess-react/sveltifyReact18\\";
const React$Counter = sveltifyReact(Counter);
</script>
<!-- Counter could be a global variable -->
<React$Counter />
"
`;

exports[`preprocess-react > should process <react:component> tags 1`] = `
"<script>import sveltifyReact from \\"svelte-preprocess-react/sveltifyReact18\\";
// @ts-nocheck
import Counter from \\"./Counter\\";
let count = 1;
const React$Counter = sveltifyReact(Counter);</script>
Expand Down
8 changes: 8 additions & 0 deletions src/tests/fixtures/Binding.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
/** eslint-disable ValidationError */
import Counter from "./Counter";
let count = 1;
</script>

<react:Counter bind:count />
2 changes: 2 additions & 0 deletions src/tests/fixtures/Container.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script>
// @ts-nocheck
import Counter from "./Counter";
let count = 1;
</script>

Expand Down
2 changes: 2 additions & 0 deletions src/tests/fixtures/NoScript.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- Counter could be a global variable -->
<react:Counter />
10 changes: 10 additions & 0 deletions src/tests/fixtures/Slots.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
// @ts-nocheck
import Counter from "./Counter";
const count = 1;
</script>

<react:Counter {count}>
<div>Hello World</div>
</react:Counter>
52 changes: 48 additions & 4 deletions src/tests/svelte-preprocess-react.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
import { describe, expect, it } from "vitest";
import { preprocess } from "svelte/compiler";
import { readFile } from "fs/promises";
import { dirname, resolve } from "path";
import preprocessReact from "../lib/svelte-preprocess-react";
import source from "./fixtures/Container.svelte?raw";

describe("preprocess-react", () => {
it("should process <react:component> tags", async () => {
const output = await preprocess(source, preprocessReact(), {
filename: "Counter.svelte",
});
const filename = resolveFilename("./fixtures/Container.svelte");
const src = await readFile(filename, "utf8");
const output = await preprocess(src, preprocessReact(), { filename });
expect(output.code).toMatchSnapshot();
});

it("should fail on bindings", async () => {
const filename = resolveFilename("./fixtures/Binding.svelte");
const src = await readFile(filename, "utf8");
let failed: boolean;
try {
await preprocess(src, preprocessReact(), { filename });
failed = false;
} catch (err: any) {
expect(err.message).toMatchSnapshot();
failed = true;
}
expect(failed).toBe(true);
});

it("should fail on slots (for now)", async () => {
const filename = resolveFilename("./fixtures/Slots.svelte");
const src = await readFile(filename, "utf8");
let failed: boolean;
try {
await preprocess(src, preprocessReact(), { filename });
failed = false;
} catch (err: any) {
expect(err.message).toBe(
"Nested components are not (yet) supported in svelte-preprocess-react"
);
failed = true;
}
expect(failed).toBe(true);
});

it("should inject a script tag", async () => {
const filename = resolveFilename("./fixtures/NoScript.svelte");
const src = await readFile(filename, "utf8");
const output = await preprocess(src, preprocessReact(), { filename });
expect(output.code).toContain("<script>");
expect(output.code).toMatchSnapshot();
});
});

const base = dirname(import.meta.url).replace("file://", "");
function resolveFilename(filename: string) {
return resolve(base, filename);
}
Loading

0 comments on commit 1123d07

Please sign in to comment.