Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Fanger committed Jun 9, 2022
0 parents commit 59b7ed1
Show file tree
Hide file tree
Showing 29 changed files with 3,651 additions and 0 deletions.
1 change: 1 addition & 0 deletions .browserslistrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
defaults
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
/build
/.svelte-kit
12 changes: 12 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": ["triple/svelte"],
"env": {
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.eslint.json",
"extraFileExtensions": [".cjs", ".svelte"]
}
}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
node_modules
/.svelte-kit
/build
/package
/functions
/storybook-static
/test-results
.env
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
4 changes: 4 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run test
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.svelte-kit
/build
/package
/node_modules
46 changes: 46 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Svelte Preprocess React

Seamlessly use React components inside a Svelte app

# "Embrace, extend, and extinguish"

This preprocessor is intended as temporary solution when migrating an existing large React codebase.
The goal should be to rewrite all the components to Svelte and remove this preprocessor from your setup.

## Usage

```html
<script>
import MyReactComponent from "./MyReactComponent.jsx";
</script>

<react:MyReactComponent />
```

## Setup

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

export default {
preprocess: preprocessReact(),
};
```

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.
77 changes: 77 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"name": "svelte-preprocess-react",
"version": "0.1.0",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"package": "svelte-kit package",
"lint": "concurrently -c \"#c596c7\",\"#676778\",\"#3074c0\",\"#7c7cea\" --kill-others-on-fail \"npm:lint:*\"",
"lint:prettier": "prettier --check --loglevel=warn \"src/**/*.svelte\"",
"lint:svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --ignore build,package",
"lint:tsc": "tsc --noEmit",
"lint:eslint": "eslint --ext=js,ts,svelte --max-warnings=0 src",
"format": "prettier --write . && eslint --ext=js,ts,svelte --fix src",
"prepare": "husky install",
"test": "vitest run --passWithNoTests",
"vitest": "vitest watch"
},
"prettier": "eslint-config-triple/.prettierrc",
"lint-staged": {
"*.ts": [
"eslint --max-warnings 0 --no-ignore",
"sh -c 'tsc -p tsconfig.json --noEmit'"
],
"*.(c)?js": [
"eslint --max-warnings 0 --no-ignore"
],
"*.svelte": [
"eslint --max-warnings 0 --no-ignore",
"svelte-check --fail-on-warnings --fail-on-hints",
"prettier --check"
]
},
"devDependencies": {
"@sveltejs/adapter-static": "next",
"@sveltejs/kit": "next",
"@testing-library/svelte": "^3.1.0",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.7",
"concurrently": "^7.2.1",
"eslint": "^8.16.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-triple": "^0.5.1",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-only-warn": "^1.0.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-svelte3": "^4.0.0",
"husky": "^8.0.1",
"jsdom": "^19.0.0",
"lint-staged": "^13.0.0",
"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",
"svelte-preprocess": "^4.10.6",
"svelte2tsx": "^0.5.10",
"typescript": "^4.7.2",
"vite": "^2.9.9",
"vite-tsconfig-paths": "^3.4.1",
"vitest": "^0.14.1"
},
"dependencies": {
"magic-string": "^0.26.2"
}
}
3 changes: 3 additions & 0 deletions postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require("autoprefixer")],
};
12 changes: 12 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body>
%sveltekit.body%
</body>
</html>
22 changes: 22 additions & 0 deletions src/demo/react-components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";

type Props = {
initialValue: number;
};
const Counter: React.FC<Props> = ({ initialValue = 0 }) => {
const [count, setCount] = React.useState(initialValue);
function decrease() {
setCount(count - 1);
}
function increase() {
setCount(count + 1);
}
return (
<>
<button onClick={decrease}>-</button>
{count}
<button onClick={increase}>+</button>
</>
);
};
export default Counter;
3 changes: 3 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />
41 changes: 41 additions & 0 deletions src/lib/React18Wrapper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { onMount } from "svelte";
import React, { createElement } from "react";
import { createRoot, type Root } from "react-dom/client";
import { renderToString } from "react-dom/server";
export let ReactComponent: React.FC;
const html = ReactComponent
? renderToString(createElement(ReactComponent, $$props))
: "";
let el: Element;
let root: Root | undefined;
$: if (root && $$props) {
rerender();
}
onMount(() => {
root = createRoot(el);
rerender();
return () => {
root?.unmount();
};
});
function rerender() {
if (!root) {
return;
}
root.render(createElement(ReactComponent, $$props));
}
</script>

<react-wrapper bind:this={el}>{@html html}</react-wrapper>

<style>
react-wrapper {
display: contents;
}
</style>
3 changes: 3 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type preprocessReact from "./svelte-preprocess-react";

export default preprocessReact;
76 changes: 76 additions & 0 deletions src/lib/svelte-preprocess-react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import MagicString from "magic-string";
import { compile } from "svelte/compiler";
import type {
Element,
Script,
TemplateNode,
Transition,
} from "svelte/types/compiler/interfaces";
import type { PreprocessorGroup } from "svelte/types/compiler/preprocess";

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

return {
markup: ({ content, filename }) => {
const compiled = compile(content, { filename });
const refs: Refs = { offset: 0, components: [] };
const s = new MagicString(content, { filename });
replaceTags(compiled.ast.html, s, refs);

if (refs.components.length === 0) {
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);
return {
code: s.toString(),
map: s.generateMap(),
};
},
};

type Refs = { offset: number; components: string[] };
function replaceTags(node: TemplateNode, content: MagicString, refs: Refs) {
/* eslint-disable no-param-reassign */
if (node.type === "Element" && node.name.startsWith("react:")) {
const tag = node as Element;
const component = tag.name.slice(6);
const tagStart = node.start + refs.offset;
content.overwrite(tagStart + 1, tagStart + 7, "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
if (event.modifiers.length > 0) {
throw new Error(
"event modifier are not (yet) supported for React components"
);
}
const eventStart = event.start + refs.offset;
content.overwrite(
eventStart,
eventStart + 4,
`on${event.name[0].toUpperCase()}`
);

refs.offset -= 1;
}
});
}
node.children?.forEach((child) => {
replaceTags(child, content, refs);
});
}
}
32 changes: 32 additions & 0 deletions src/lib/sveltifyReact18.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ComponentClass, FunctionComponent } from "react";
import ReactWrapper from "./React18Wrapper.svelte";

export default function sveltifyReact<P>(
ReactComponent: FunctionComponent<P> | ComponentClass<P>
): (props: P) => any {
const ssr = typeof (ReactWrapper as any).$$render === "function";
if (ssr) {
const { $$render } = ReactWrapper as any;
return {
...ReactWrapper,
$$render(meta: any, props: any, ...args: any[]) {
const result = $$render.call(
ReactWrapper,
meta,
{ ReactComponent, ...props },
...args
);
return result;
},
} as any;
}

function Sveltified(options: any) {
const component = new ReactWrapper({
...options,
props: { ReactComponent, ...options.props },
});
return component;
}
return Sveltified as any;
}
Loading

0 comments on commit 59b7ed1

Please sign in to comment.