Skip to content

Commit

Permalink
feat: Support for <react.Component.Item /> + automatic injection of c…
Browse files Browse the repository at this point in the history
…omponents to sveltify(...)

-  `<react.div>` now even works when no `div:"div"` is passed to sveltify.

Fixes #46
  • Loading branch information
bfanger committed Nov 17, 2024
1 parent b1f0b79 commit 80c10f9
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 48 deletions.
19 changes: 11 additions & 8 deletions src/lib/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Component } from "svelte";
import type { Sveltified } from "./internal/types";
import type { Readable } from "svelte/store";
import type {
IntrinsicElementComponents,
StaticPropComponents,
Sveltified,
} from "./internal/types.js";

declare global {
function sveltify<
Expand All @@ -9,16 +14,14 @@ declare global {
| React.JSXElementConstructor<any>;
},
>(
components: T,
reactComponents: T,
): {
[K in keyof T]: K extends keyof JSX.IntrinsicElements
? Sveltified[K]
: Sveltified<T[K]>;
};
[K in keyof T]: Sveltified<T[K]> & StaticPropComponents;
} & IntrinsicElementComponents;

function hooks<T>(callback: () => T): Readable<T | undefined>;

const react: {
[component: string]: Component;
const react: IntrinsicElementComponents & {
[component: string]: Component & StaticPropComponents;
};
}
18 changes: 1 addition & 17 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
/// <reference path="global.d.ts" />

import type { Sveltified } from "./internal/types.js";
/// <reference types="./global" />
export { default as hooks } from "./hooks.js";
export { default as reactify } from "./reactify.js";
export { default as sveltify } from "./sveltify.svelte.js";
export { default as used } from "./used.js";
export { default as useStore } from "./useStore.js";

declare global {
function sveltify<
T extends {
[key: string]:
| keyof JSX.IntrinsicElements
| React.JSXElementConstructor<any>;
},
>(
reactComponents: T,
): {
[K in keyof T]: Sveltified<T[K]>;
};
}
11 changes: 11 additions & 0 deletions src/lib/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ export type Sveltified<
T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>,
> = Component<ChildrenPropsAsSnippet<React.ComponentProps<T>>>;

export type IntrinsicElementComponents = {
[K in keyof JSX.IntrinsicElements]: Component<
ChildrenPropsAsSnippet<React.ComponentProps<K>>
>;
};

/* Primitive typing of `Component.Item` components */
export type StaticPropComponents = {
[key: string]: Component<any> & StaticPropComponents;
};

export type ReactDependencies = {
ReactDOM?:
| {
Expand Down
82 changes: 64 additions & 18 deletions src/lib/preprocessReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default function preprocessReact(options = {}) {
},
};
}
const prefix = "inject$$";

/**
* @param {string} content
Expand All @@ -74,7 +75,6 @@ export default function preprocessReact(options = {}) {
* @returns
*/
function transform(content, options) {
const prefix = "inject$$";
/** @type {string} */
let portal;
const packageName = "svelte-preprocess-react";
Expand Down Expand Up @@ -112,9 +112,8 @@ function transform(content, options) {
/** @type {Set<'sveltify' | 'hooks'>} */
let used = new Set();
let defined = false;
// import ReactDOMClient from "react-dom/client"; // React 18+,(use "react-dom" for older versions)
// import { renderToString } from "react-dom/server";
// ReactDOMClient, renderToString
/** @type {false|Set<string>} */
let aliased = false;

/**
* Detect sveltify import and usage
Expand All @@ -137,6 +136,30 @@ function transform(content, options) {
) {
s.appendRight(parent.arguments[0].end, `, { ${deps.join(", ")} }`);
}

const componentsArg = parent.arguments[0];
if (!aliased && componentsArg.type === "ObjectExpression") {
aliased = new Set();
for (const property of componentsArg.properties) {
if (property.type === "Property") {
if (property.key.type === "Identifier") {
aliased.add(property.key.name);
}
}
}
if ("end" in componentsArg && typeof componentsArg.end === "number") {
for (const [alias, { expression }] of aliases) {
if (!aliased.has(alias)) {
s.appendRight(
componentsArg.end - 1,
`, ${alias}: ${expression === expression.toLowerCase() ? JSON.stringify(expression) : expression} `,
);
}
}
} else {
console.warn("missing end in Node<ObjectExpression>");
}
}
used.add("sveltify");
}
}
Expand Down Expand Up @@ -192,12 +215,15 @@ function transform(content, options) {
let wrappers = [];
if (!defined && aliases.length > 0) {
wrappers.push(
`const react = sveltify({ ${Object.keys(components)
.map((component) => {
if (component.toLowerCase() === component) {
return `${component.match(/^[a-z]+$/) ? component : JSON.stringify(component)}: ${JSON.stringify(component)}`;
`const react = sveltify({ ${Object.entries(components)
.map(([alias, { expression }]) => {
if (expression !== alias) {
return `${alias}: ${expression}`;
}
return component;
if (expression.toLowerCase() === expression) {
return `${expression.match(/^[a-z]+$/) ? expression : JSON.stringify(expression)}: ${JSON.stringify(expression)}`;
}
return expression;
})
.join(", ")} }, { ${deps.join(", ")} });`,
);
Expand Down Expand Up @@ -231,14 +257,15 @@ function transform(content, options) {
* @param {any} node
* @param {MagicString} content
* @param {string | undefined} filename
* @param {Record<string, { dispatcher: boolean }>} components
* @param {Record<string, { dispatcher: boolean, expression: string }>} components
*/
function replaceReactTags(node, content, filename, components = {}) {
if (
(node.type === "Element" && node.name.startsWith("react:")) ||
(node.type === "InlineComponent" && node.name.startsWith("react."))
) {
let legacy = node.name.startsWith("react:");

if (legacy) {
let location = "";
if (filename) {
Expand All @@ -250,15 +277,34 @@ function replaceReactTags(node, content, filename, components = {}) {
console.warn(
`'<${node.name}' syntax is deprecated, use '<react.${node.name.substring(6)}'${location}.\nhttps://github.com/bfanger/svelte-preprocess-react/blob/main/docs/migration-to-2.0.md\n`,
);
content.overwrite(node.start + 6, node.start + 7, ".");
}
const expression = node.name.slice(6).replace("[.].*", "");
const alias =
expression.indexOf(".") === -1
? expression
: `${prefix}${expression.replace(/\./g, "$")}`;
if (legacy || expression !== alias) {
let tagPrefix = legacy ? "react:" : "react.";
const tagEnd = node.end - node.name.length - 3;
if (content.slice(tagEnd, tagEnd + 8) === `</react:`) {
content.overwrite(tagEnd + 7, tagEnd + 8, ".");
const hasClosingTag =
content.slice(tagEnd, tagEnd + 8) === `</${tagPrefix}`;

content.overwrite(
node.start + 1,
node.start + 7 + expression.length,
`react.${alias}`,
);
if (hasClosingTag) {
content.overwrite(
tagEnd + 2,
tagEnd + 8 + expression.length,
`react.${alias}`,
);
}
}
const identifier = node.name.slice(6).replace("[.].*", "");
if (!components[identifier]) {
components[identifier] = { dispatcher: false };

if (!components[alias]) {
components[alias] = { dispatcher: false, expression };
}

node.attributes.forEach((/** @type {any} */ attr) => {
Expand All @@ -282,11 +328,11 @@ function replaceReactTags(node, content, filename, components = {}) {
event.name[0].toUpperCase() + event.name.substring(1)
}={(e) => React$$dispatch(${JSON.stringify(event.name)}, e)}`,
);
components[identifier].dispatcher = true;
components[alias].dispatcher = true;
}
}
});
if (node.children) {
if (node.children && !legacy) {
if (node.children.length === 0) {
const childrenProp =
Array.isArray(node.attributes) &&
Expand Down
6 changes: 4 additions & 2 deletions src/lib/sveltify.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from "react";
import type {
ChildrenPropsAsSnippet,
IntrinsicElementComponents,
ReactDependencies,
StaticPropComponents,
SvelteInit,
Sveltified,
TreeNode,
Expand All @@ -25,8 +27,8 @@ function sveltify<
components: T,
dependencies?: ReactDependencies,
): {
[K in keyof T]: Sveltified<T[K]>;
};
[K in keyof T]: Sveltified<T[K]> & StaticPropComponents;
} & IntrinsicElementComponents;
/**
* Convert a React component into a Svelte component.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/routes/listitem/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import List from "../../tests/fixtures/List.svelte";
</script>

<List />

<!-- <react.div><react.h1>Heading1</react.h1></react.div> -->
<!-- <react.h2>Heading2</react.h2> -->
<!-- <react.h1>Heading1</react.h1>
<react.h2>Heading2</react.h2>
<react.h3>Heading3</react.h3> -->
6 changes: 3 additions & 3 deletions src/routes/preprocessor/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Alert from "../../demo/react-components/Alert";
import Counter from "../../demo/react-components/Counter";
const react = sveltify({ Counter, Alert, div: "div" });
const react = sveltify({ Counter, Alert });
</script>

<react.Counter initial={10} onCount={console.info} /><br />
Expand All @@ -13,6 +13,6 @@

<react.Alert>
"Multiline content". {10 ** 4} Lorem ipsum dolor sit amet consectetur adipisicing
elit. Suscipit nisi atque asperiores.</react.Alert
>
elit. Suscipit nisi atque asperiores.
</react.Alert>
<react.div>a div</react.div>
15 changes: 15 additions & 0 deletions src/tests/__snapshots__/preprocess.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ exports[`svelte-preprocess-react > should portal slotted content as children 1`]
"
`;

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$$createPortal, inject$$ReactDOM, inject$$renderToString });
;</script>
<react.List>
<react.inject$$List$Item label="1" />
<react.inject$$List$Item react$children="Item 2" />
<react.inject$$List$Item>Item <span>3</span></react.inject$$List$Item>
</react.List>
"
`;
exports[`svelte-preprocess-react > should process <react:Context.Provider> 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 type { Context as ContentType } from "react";
Expand Down
11 changes: 11 additions & 0 deletions src/tests/fixtures/List.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import List from "./List";
const react = sveltify({ List });
</script>

<react.List>
<react.List.Item label="1" />
<react.List.Item>Item 2</react.List.Item>
<react.List.Item>Item <span>3</span></react.List.Item>
</react.List>
22 changes: 22 additions & 0 deletions src/tests/fixtures/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";

type Props = {
children: React.ReactNode;
};
export default function List({ children }: Props) {
return <ul className="list">{children}</ul>;
}

type ItemProps = {
label: string;
children: React.ReactNode;
};

List.Item = ({ label, children }: ItemProps) => {
return (
<li className="list__item">
{label}
{children}
</li>
);
};
7 changes: 7 additions & 0 deletions src/tests/preprocess.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ describe("svelte-preprocess-react", () => {
);
expect(output.code).toMatchSnapshot();
});

it("should process <react.Component.Item> tags", async () => {
const filename = resolveFilename("./fixtures/List.svelte");
const src = await readFile(filename, "utf8");
const output = await preprocess(src, preprocessReact(), { filename });
expect(output.code).toMatchSnapshot();
});
});

const base = dirname(fileURLToPath(import.meta.url));
Expand Down

0 comments on commit 80c10f9

Please sign in to comment.