From ae2e07595f0a9de5d52c4e84d192bf2e36ddcebe Mon Sep 17 00:00:00 2001 From: Mohamad Mohebifar Date: Tue, 12 Mar 2024 09:00:03 -0400 Subject: [PATCH] docs: "Un"compare Forgetti with Unforget --- .../docs/components/BeforeAfterCodeLayout.tsx | 4 +- apps/docs/components/LiveCodeSandpack.tsx | 99 +++++++---- apps/docs/package.json | 1 + apps/docs/pages/comparisons/_meta.json | 8 + apps/docs/pages/comparisons/forgetti.mdx | 154 ++++++++++++++++++ .../src/models/segment/ComponentSegment.ts | 4 - .../optimize-segment-dependencies.ts | 3 - .../utils/scope-tools/preserve-references.ts | 4 - yarn.lock | 32 ++-- 9 files changed, 251 insertions(+), 58 deletions(-) create mode 100644 apps/docs/pages/comparisons/_meta.json diff --git a/apps/docs/components/BeforeAfterCodeLayout.tsx b/apps/docs/components/BeforeAfterCodeLayout.tsx index ecb0d92..127747e 100644 --- a/apps/docs/components/BeforeAfterCodeLayout.tsx +++ b/apps/docs/components/BeforeAfterCodeLayout.tsx @@ -4,11 +4,13 @@ import { PiNumberCircleOneFill, PiNumberCircleTwoFill } from "react-icons/pi"; export interface BeforeAfterCodeLayoutProps { before: React.ReactNode; after: React.ReactNode; + afterTitle?: React.ReactNode; } export function BeforeAfterCodeLayout({ before, after, + afterTitle = "React Unforget Result", }: BeforeAfterCodeLayoutProps) { return (
@@ -37,7 +39,7 @@ export function BeforeAfterCodeLayout({
- React Unforget Result + {afterTitle}
diff --git a/apps/docs/components/LiveCodeSandpack.tsx b/apps/docs/components/LiveCodeSandpack.tsx index fce96e0..37ae892 100644 --- a/apps/docs/components/LiveCodeSandpack.tsx +++ b/apps/docs/components/LiveCodeSandpack.tsx @@ -6,6 +6,7 @@ import reactUnforgetBabelPlugin, { // @ts-ignore import jsxBabelPlugin from "@babel/plugin-syntax-jsx"; +import forgettiBabel from "forgetti"; import { useTheme } from "nextra-theme-docs"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BeforeAfterCodeLayout } from "./BeforeAfterCodeLayout"; @@ -32,9 +33,14 @@ export default function App() { export interface LiveCodeProps { children: string; previewClassName?: string; + plugin?: "unforget" | "forgetti"; } -function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { +function LiveCodeSandpack({ + children, + previewClassName, + plugin = "unforget", +}: LiveCodeProps) { const [beforeCode, setBeforeCode] = useState(children); const [afterCode, setAfterCode] = useState(defaultLoadingCode); const [mermaidSyntax, setMermaidSyntax] = useState(null); @@ -45,12 +51,15 @@ function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { const { theme } = useTheme(); const transformCode = useCallback((content: string) => { + const babelPluginToUse = + plugin === "unforget" ? reactUnforgetBabelPlugin : forgettiBabel; const result = transform(content, { plugins: [ jsxBabelPlugin, [ - reactUnforgetBabelPlugin, + babelPluginToUse, { + preset: "react", _debug_onComponentAnalysisFinished: (component: any) => { setMermaidSyntax(mermaidGraphFromComponent(component)); }, @@ -62,6 +71,16 @@ function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { return result?.code ?? ""; }, []); + const customSetup = useMemo( + () => ({ + dependencies: { + [plugin === "unforget" ? "@react-unforget/runtime" : "forgetti"]: + "latest", + }, + }), + [], + ); + const handleCodeChange = useCallback((newCode: string) => { setBeforeCode(newCode); }, []); @@ -116,40 +135,52 @@ function LiveCodeSandpack({ children, previewClassName }: LiveCodeProps) { } after={ - - - +
+ + {plugin === "forgetti" ? ( +
+ The following editor is a preview of Forgetti not Unforget +
+ ) : null} + +
+
+ } + afterTitle={ + plugin === "forgetti" ? ( + + Forgetti Result + + ) : undefined } /> -
- - Click here to {viewDependencyGraph ? "hide" : "view"} the dependency - graph - -
- {viewDependencyGraph && ( - - )} -
-
+ {plugin === "unforget" && ( +
+ + Click here to {viewDependencyGraph ? "hide" : "view"} the dependency + graph + +
+ {viewDependencyGraph && ( + + )} +
+
+ )}
); } diff --git a/apps/docs/package.json b/apps/docs/package.json index 4888d00..0eba2f2 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -16,6 +16,7 @@ "@react-unforget/runtime": "0.1.0-alpha.7", "@vercel/analytics": "^1.2.2", "clsx": "^2.1.0", + "forgetti": "^0.8.4", "framer-motion": "^11.0.8", "html-entities": "^2.5.2", "mermaid": "^10.9.0", diff --git a/apps/docs/pages/comparisons/_meta.json b/apps/docs/pages/comparisons/_meta.json new file mode 100644 index 0000000..2b13bc5 --- /dev/null +++ b/apps/docs/pages/comparisons/_meta.json @@ -0,0 +1,8 @@ +{ + "forgetti": { + "title": "Forgetti vs. Unforget", + "theme": { + "layout": "full" + } + } +} diff --git a/apps/docs/pages/comparisons/forgetti.mdx b/apps/docs/pages/comparisons/forgetti.mdx index 32e3bbb..4b9924c 100644 --- a/apps/docs/pages/comparisons/forgetti.mdx +++ b/apps/docs/pages/comparisons/forgetti.mdx @@ -1,3 +1,5 @@ +import { DynamicLiveCodeSandpack } from "@components/DynamicLiveCodeSandpack"; + # Forgetti [Forgetti](https://github.com/lxsmnsyc/forgetti) is also another alternative tool made to optimize React components at build time to make it run faster at runtime. But, when it comes to more complicated patterns, it generates failing code. For example, loops and mutations can lead to generating code that does not work. @@ -9,3 +11,155 @@ | Breaking down JSX | ✅ | ✖️ | | Basic mutation detection | ✅ | ✖️ | | Control flows | ✅ | Fails with while/for loops | + +### Example + +Ok, let's see how Forgetti performs with a simple example. We have a counter component that uses the `useState` hook. We will see how Forgetti handles this. + + +{`import { useState } from "react"; + +export default function CounterWithMutationTracking() { + const [state, setState] = useState(0); + + const text = "Yay!"; + + return ( +
+ +
+ Count: {state} {text} +
+
+ ); +} +`} + +
+ +Yay 🎉! It worked. Now let's make it a bit more complex. Remember the example from the home page of Unforget? Let's make the test mutable. + + +```ts +let text = "The count is is: "; + +if (state % 2 === 0) { + text += "even"; +} else { + text += "odd"; +} +``` + + +{`import { useState } from "react"; + +export default function CounterWithMutationTracking() { + const [state, setState] = useState(0); + + let text = "The number is: "; + + if (state % 2 === 0) { + text += "even"; + } else { + text += "odd"; + } + + return ( +
+ +
+ Count: {state} {text} +
+
+ ); +} +`} + +
+ +And that just failed. Forgetti is not able to handle this. It's not able to detect the mutation in the `text` variable. This is a limitation of Forgetti. + +Now let's see how it handles loops. + + + +{`import { useState } from "react"; + +const useData = () => { + const [data, setData] = useState([]); + + return { + data, + addData: (item) => { + setData([...data, item]); + }, + }; +}; + +export default function CounterWithMutationTracking() { + const { data, addData } = useData(); + + const filteredData = []; + for (let i = 0; i < data.length; i++) { + if (data[i] % 2 === 0) { + filteredData.push(data[i]); + } + } + + return ( +
+ +
+ Data:{" "} + {data.map((item) => ( + {item} + ))} +
+
+ Filtered data:{" "} + {filteredData.map((item) => ( + {item} + ))} +
+
+ ); +} +`} +
+ +Oh no! It failed again. + +Ok, one more test. Let's see how it handles alias analysis. + + + +{`import { useState } from "react"; + +function Comp({ a, b }) { + const x = []; + + x.push(a); + + const y = x; + + y.push(b); + + return
n: {x.join(",")}
; +} + +export default function App() { + const [state, setState] = useState(1); + + return ( +
+ {/* We use a constant value for a, but change b */} + + +
+ ); +} +`} + +
+ +And it failed again. Click on the button and you will see that the value is not updated. \ No newline at end of file diff --git a/packages/babel-plugin/src/models/segment/ComponentSegment.ts b/packages/babel-plugin/src/models/segment/ComponentSegment.ts index f64fe11..c921043 100644 --- a/packages/babel-plugin/src/models/segment/ComponentSegment.ts +++ b/packages/babel-plugin/src/models/segment/ComponentSegment.ts @@ -573,10 +573,6 @@ export class ComponentSegment { return hasHookCall(this.path, this.component.path); } - isTransformable() { - return !this.hasHookCall() && this.isInReturnNetwork(); - } - /** * Apply the transformation to the segment */ diff --git a/packages/babel-plugin/src/utils/model-tools/optimize-segment-dependencies.ts b/packages/babel-plugin/src/utils/model-tools/optimize-segment-dependencies.ts index 94a77fb..052c9a3 100644 --- a/packages/babel-plugin/src/utils/model-tools/optimize-segment-dependencies.ts +++ b/packages/babel-plugin/src/utils/model-tools/optimize-segment-dependencies.ts @@ -36,7 +36,4 @@ export function optimizeSegmentDependencies(dependencies: SegmentDependency[]) { }); return optimizedDependencies; - return optimizedDependencies.filter((dependency) => - dependency.segment.isTransformable(), - ); } diff --git a/packages/babel-plugin/src/utils/scope-tools/preserve-references.ts b/packages/babel-plugin/src/utils/scope-tools/preserve-references.ts index 68a1624..3121461 100644 --- a/packages/babel-plugin/src/utils/scope-tools/preserve-references.ts +++ b/packages/babel-plugin/src/utils/scope-tools/preserve-references.ts @@ -14,10 +14,6 @@ export function preserveReferences(binding: Binding) { newBinding.reference(reference); }); } else { - console.error({ - oldName, - // oldReferences: oldReferences.map((r) => r.getSource()), - }); throw new Error("Binding not found"); } }; diff --git a/yarn.lock b/yarn.lock index 74745aa..4244276 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1039,10 +1039,10 @@ "@babel/parser" "^7.23.9" "@babel/types" "^7.23.9" -"@babel/traverse@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" - integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== +"@babel/traverse@^7.23.2", "@babel/traverse@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" + integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -1050,15 +1050,15 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" - integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -1066,8 +1066,8 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.24.0" - "@babel/types" "^7.24.0" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" debug "^4.3.1" globals "^11.1.0" @@ -5256,6 +5256,14 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +forgetti@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/forgetti/-/forgetti-0.8.4.tgz#69d3415855389fc8c487cdd74bf5b12d59993fef" + integrity sha512-F3oCFWHbn3WoUakc1O4sWOPeSDJyB9/IehoKZ7YT2AEtHJYMA5E7XMiPX4f/Z8vXTJ9rkTDYXynleSeP6VdxJA== + dependencies: + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"