From 74be8e38e9f91d14c7085a1382051b25772b87a1 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Fri, 3 Nov 2023 16:20:18 +1100 Subject: [PATCH 01/10] use smaller React pragmas to minimise the amount of data passed between iframes --- src/Playroom/Frames/Frames.tsx | 23 ++++++++++++++++------- src/Playroom/RenderCode/RenderCode.js | 7 +++++++ src/utils/compileJsx.ts | 19 +++++++++++++++---- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/Playroom/Frames/Frames.tsx b/src/Playroom/Frames/Frames.tsx index 9dbec9dc..79de0932 100644 --- a/src/Playroom/Frames/Frames.tsx +++ b/src/Playroom/Frames/Frames.tsx @@ -1,7 +1,11 @@ -import { useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import flatMap from 'lodash/flatMap'; import Iframe from './Iframe'; -import { compileJsx } from '../../utils/compileJsx'; +import { + compileJsx, + openFragmentTag, + closeFragmentTag, +} from '../../utils/compileJsx'; import type { PlayroomProps } from '../Playroom'; import { Strong } from '../Strong/Strong'; import { Text } from '../Text/Text'; @@ -16,8 +20,6 @@ interface FramesProps { widths: PlayroomProps['widths']; } -let renderCode = ''; - export default function Frames({ code, themes, widths }: FramesProps) { const scrollingPanelRef = useRef(null); @@ -29,9 +31,16 @@ export default function Frames({ code, themes, widths }: FramesProps) { })) ); - try { - renderCode = compileJsx(code); - } catch (e) {} + const [renderCode, setRenderCode] = useState( + () => `${openFragmentTag}${closeFragmentTag}` + ); + + useEffect(() => { + try { + const newCode = compileJsx(code); + setRenderCode(newCode); + } catch (e) {} + }, [code]); return (
diff --git a/src/Playroom/RenderCode/RenderCode.js b/src/Playroom/RenderCode/RenderCode.js index 73522fae..5a753f50 100644 --- a/src/Playroom/RenderCode/RenderCode.js +++ b/src/Playroom/RenderCode/RenderCode.js @@ -4,10 +4,17 @@ import scopeEval from 'scope-eval'; // eslint-disable-next-line import/no-unresolved import useScope from '__PLAYROOM_ALIAS__USE_SCOPE__'; +import { + ReactCreateElementPragma, + ReactFragmentPragma, +} from '../../utils/compileJsx'; + export default function RenderCode({ code, scope }) { return scopeEval(code, { ...(useScope() ?? {}), ...scope, React, + [ReactCreateElementPragma]: React.createElement, + [ReactFragmentPragma]: React.Fragment, }); } diff --git a/src/utils/compileJsx.ts b/src/utils/compileJsx.ts index 97aa4737..f67da928 100644 --- a/src/utils/compileJsx.ts +++ b/src/utils/compileJsx.ts @@ -1,11 +1,22 @@ import { transform } from '@babel/standalone'; -export const openFragmentTag = ''; -export const closeFragmentTag = ''; +export const ReactFragmentPragma = 'R_F'; +export const ReactCreateElementPragma = 'R_cE'; + +export const openFragmentTag = '<>'; +export const closeFragmentTag = ''; export const compileJsx = (code: string) => - transform(`${openFragmentTag}${code.trim() || ''}${closeFragmentTag}`, { - presets: ['react'], + transform(`${openFragmentTag}${code.trim()}${closeFragmentTag}`, { + presets: [ + [ + 'react', + { + pragma: ReactCreateElementPragma, + pragmaFrag: ReactFragmentPragma, + }, + ], + ], }).code; export const validateCode = (code: string) => { From 2368d8e439552be34950d28a10b346ef6b8a0078 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 09:43:45 +1100 Subject: [PATCH 02/10] bump `use-debounce` to fix type errors --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- src/Playroom/CodeEditor/CodeEditor.tsx | 1 - src/Playroom/Playroom.tsx | 1 - src/Playroom/Snippets/Snippets.tsx | 1 - src/StoreContext/StoreContext.tsx | 1 - 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 38b3b4f7..829bfbbe 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "scope-eval": "^1.0.0", "sucrase": "^3.34.0", "typescript": ">=5.0.0", - "use-debounce": "^9.0.2", + "use-debounce": "^9.0.4", "webpack": "^5.75.0", "webpack-dev-server": "^4.11.1", "webpack-merge": "^5.8.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf326ba0..197e126e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ specifiers: sucrase: ^3.34.0 surge: ^0.23.1 typescript: '>=5.0.0' - use-debounce: ^9.0.2 + use-debounce: ^9.0.4 webpack: ^5.75.0 webpack-dev-server: ^4.11.1 webpack-merge: ^5.8.0 @@ -123,7 +123,7 @@ dependencies: scope-eval: 1.0.0 sucrase: 3.34.0 typescript: 5.0.4 - use-debounce: 9.0.2_react@18.2.0 + use-debounce: 9.0.4_react@18.2.0 webpack: 5.75.0 webpack-dev-server: 4.11.1_webpack@5.75.0 webpack-merge: 5.8.0 @@ -9702,8 +9702,8 @@ packages: resolution: {integrity: sha1-+4CQGIPzOLPL7TU49fqiatr38uc=} dev: true - /use-debounce/9.0.2_react@18.2.0: - resolution: {integrity: sha512-QLyB0sxt9F5AisGDrUybCRJSLE60bTQR0yXc+IebNGUu1GCXwii1zsZl82mPGdWqDVQy7+1FKMLHQUixxf5Nbw==} + /use-debounce/9.0.4_react@18.2.0: + resolution: {integrity: sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==} engines: {node: '>= 10.0.0'} peerDependencies: react: '>=16.8.0' diff --git a/src/Playroom/CodeEditor/CodeEditor.tsx b/src/Playroom/CodeEditor/CodeEditor.tsx index a57ec53d..6539d206 100644 --- a/src/Playroom/CodeEditor/CodeEditor.tsx +++ b/src/Playroom/CodeEditor/CodeEditor.tsx @@ -1,5 +1,4 @@ import { useRef, useContext, useEffect, useCallback } from 'react'; -// @ts-expect-error no types import { useDebouncedCallback } from 'use-debounce'; import type { Editor } from 'codemirror'; import 'codemirror/lib/codemirror.css'; diff --git a/src/Playroom/Playroom.tsx b/src/Playroom/Playroom.tsx index 55af5b84..af6bcfe4 100644 --- a/src/Playroom/Playroom.tsx +++ b/src/Playroom/Playroom.tsx @@ -1,6 +1,5 @@ import { useContext, type ComponentType, Fragment } from 'react'; import classnames from 'classnames'; -// @ts-expect-error no types import { useDebouncedCallback } from 'use-debounce'; import { Resizable } from 're-resizable'; import Frames from './Frames/Frames'; diff --git a/src/Playroom/Snippets/Snippets.tsx b/src/Playroom/Snippets/Snippets.tsx index 6689e311..4d2a9f51 100644 --- a/src/Playroom/Snippets/Snippets.tsx +++ b/src/Playroom/Snippets/Snippets.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useMemo, useRef } from 'react'; import classnames from 'classnames'; import fuzzy from 'fuzzy'; -// @ts-expect-error no types import { useDebouncedCallback } from 'use-debounce'; import type { PlayroomProps } from '../Playroom'; import type { Snippet } from '../../../utils'; diff --git a/src/StoreContext/StoreContext.tsx b/src/StoreContext/StoreContext.tsx index d25c946b..4065ee51 100644 --- a/src/StoreContext/StoreContext.tsx +++ b/src/StoreContext/StoreContext.tsx @@ -9,7 +9,6 @@ import copy from 'copy-to-clipboard'; import localforage from 'localforage'; import lzString from 'lz-string'; import dedent from 'dedent'; -// @ts-expect-error no types import { useDebouncedCallback } from 'use-debounce'; import { type Snippet, compressParams } from '../../utils'; From 2e889ebaf5f241056c7738c94ad43574cddb61bb Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 11:41:13 +1100 Subject: [PATCH 03/10] validate code with Babel --- package.json | 2 + pnpm-lock.yaml | 103 +++++++++++++------------ src/Playroom/CodeEditor/CodeEditor.tsx | 17 ++-- src/Playroom/Frames/Frames.tsx | 14 +--- src/utils/compileJsx.test.ts | 65 ++++++++++++++++ src/utils/compileJsx.ts | 25 +++++- src/utils/cursor.ts | 2 +- 7 files changed, 154 insertions(+), 74 deletions(-) create mode 100644 src/utils/compileJsx.test.ts diff --git a/package.json b/package.json index 829bfbbe..48a4f4df 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", + "@babel/standalone": "^7.23.3", "@soda/friendly-errors-webpack-plugin": "^1.8.1", "@types/base64-url": "^2.2.0", "@types/codemirror": "^5.60.5", @@ -114,6 +115,7 @@ "@actions/core": "^1.10.0", "@changesets/cli": "^2.25.2", "@octokit/rest": "^19.0.5", + "@types/babel__standalone": "^7.1.7", "@types/jest": "^29.2.4", "concurrently": "^7.6.0", "cypress": "^12.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 197e126e..c1c40aa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,9 +7,11 @@ specifiers: '@babel/preset-env': ^7.20.2 '@babel/preset-react': ^7.18.6 '@babel/preset-typescript': ^7.18.6 + '@babel/standalone': ^7.23.3 '@changesets/cli': ^2.25.2 '@octokit/rest': ^19.0.5 '@soda/friendly-errors-webpack-plugin': ^1.8.1 + '@types/babel__standalone': ^7.1.7 '@types/base64-url': ^2.2.0 '@types/codemirror': ^5.60.5 '@types/dedent': ^0.7.0 @@ -78,6 +80,7 @@ dependencies: '@babel/preset-env': 7.20.2_@babel+core@7.20.5 '@babel/preset-react': 7.18.6_@babel+core@7.20.5 '@babel/preset-typescript': 7.18.6_@babel+core@7.20.5 + '@babel/standalone': 7.23.3 '@soda/friendly-errors-webpack-plugin': 1.8.1_webpack@5.75.0 '@types/base64-url': 2.2.0 '@types/codemirror': 5.60.5 @@ -132,6 +135,7 @@ devDependencies: '@actions/core': 1.10.0 '@changesets/cli': 2.25.2 '@octokit/rest': 19.0.5 + '@types/babel__standalone': 7.1.7 '@types/jest': 29.2.4 concurrently: 7.6.0 cypress: 12.0.2 @@ -278,7 +282,7 @@ packages: resolution: {integrity: sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 @@ -296,7 +300,7 @@ packages: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-annotate-as-pure/7.22.5: @@ -311,7 +315,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-compilation-targets/7.20.0_@babel+core@7.20.5: @@ -398,7 +402,7 @@ packages: resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-function-name/7.19.0: @@ -406,7 +410,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.18.10 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/helper-function-name/7.22.5: resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} @@ -420,7 +424,7 @@ packages: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/helper-hoist-variables/7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} @@ -433,14 +437,14 @@ packages: resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-module-imports/7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/helper-module-imports/7.22.5: resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} @@ -457,10 +461,10 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/helper-simple-access': 7.20.2 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-validator-identifier': 7.22.5 '@babel/template': 7.18.10 '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color @@ -484,7 +488,7 @@ packages: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-plugin-utils/7.20.2: @@ -506,7 +510,7 @@ packages: '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: false @@ -519,7 +523,7 @@ packages: '@babel/helper-member-expression-to-functions': 7.18.9 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: false @@ -528,7 +532,7 @@ packages: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/helper-simple-access/7.22.5: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} @@ -541,14 +545,14 @@ packages: resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/helper-split-export-declaration/7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/helper-split-export-declaration/7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} @@ -557,23 +561,13 @@ packages: '@babel/types': 7.22.5 dev: true - /@babel/helper-string-parser/7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - /@babel/helper-string-parser/7.22.5: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-identifier/7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} /@babel/helper-validator-identifier/7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option/7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} @@ -591,7 +585,7 @@ packages: '@babel/helper-function-name': 7.19.0 '@babel/template': 7.18.10 '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: false @@ -602,7 +596,7 @@ packages: dependencies: '@babel/template': 7.18.10 '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color @@ -621,7 +615,7 @@ packages: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-validator-identifier': 7.22.5 chalk: 2.4.2 js-tokens: 4.0.0 @@ -639,7 +633,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 /@babel/parser/7.22.7: resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} @@ -647,7 +641,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.22.5 - dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.5: resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} @@ -1225,7 +1218,7 @@ packages: '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-module-transforms': 7.20.2 '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-validator-identifier': 7.22.5 transitivePeerDependencies: - supports-color dev: false @@ -1348,7 +1341,7 @@ packages: '@babel/helper-module-imports': 7.18.6 '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: false /@babel/plugin-transform-react-jsx/7.22.5_@babel+core@7.22.8: @@ -1589,7 +1582,7 @@ packages: '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.5 '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 esutils: 2.0.3 dev: false @@ -1643,13 +1636,18 @@ packages: dependencies: regenerator-runtime: 0.13.11 + /@babel/standalone/7.23.3: + resolution: {integrity: sha512-ZfB6wyLVqr9ANl1F0l/0aqoNUE1/kcWlQHmk0wF9OTEKDK1whkXYLruRMt53zY556yS2+84EsOpr1hpjZISTRg==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 /@babel/template/7.22.5: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} @@ -1670,8 +1668,8 @@ packages: '@babel/helper-function-name': 7.19.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: @@ -1699,8 +1697,8 @@ packages: resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 /@babel/types/7.22.5: @@ -1710,7 +1708,6 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 - dev: true /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2521,8 +2518,8 @@ packages: /@types/babel__core/7.1.20: resolution: {integrity: sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.3 @@ -2531,20 +2528,26 @@ packages: /@types/babel__generator/7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 + dev: true + + /@types/babel__standalone/7.1.7: + resolution: {integrity: sha512-4RUJX9nWrP/emaZDzxo/+RYW8zzLJTXWJyp2k78HufG459HCz754hhmSymt3VFOU6/Wy+IZqfPvToHfLuGOr7w==} + dependencies: + '@types/babel__core': 7.1.20 dev: true /@types/babel__template/7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 dev: true /@types/babel__traverse/7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 dev: true /@types/base64-url/2.2.0: @@ -3560,7 +3563,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.18.10 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 '@types/babel__core': 7.1.20 '@types/babel__traverse': 7.18.3 dev: true @@ -6416,7 +6419,7 @@ packages: engines: {node: '>=8'} dependencies: '@babel/core': 7.20.5 - '@babel/parser': 7.20.5 + '@babel/parser': 7.22.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.0 @@ -6812,7 +6815,7 @@ packages: '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5 '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.5 '@babel/traverse': 7.20.5 - '@babel/types': 7.20.5 + '@babel/types': 7.22.5 '@jest/expect-utils': 29.3.1 '@jest/transform': 29.3.1 '@jest/types': 29.3.1 diff --git a/src/Playroom/CodeEditor/CodeEditor.tsx b/src/Playroom/CodeEditor/CodeEditor.tsx index 6539d206..7f34ff2d 100644 --- a/src/Playroom/CodeEditor/CodeEditor.tsx +++ b/src/Playroom/CodeEditor/CodeEditor.tsx @@ -11,8 +11,8 @@ import { import { formatCode as format, isMac } from '../../utils/formatting'; import { closeFragmentTag, - compileJsx, openFragmentTag, + validateCode, } from '../../utils/compileJsx'; import * as styles from './CodeEditor.css'; @@ -41,13 +41,12 @@ import { } from './keymaps/cursors'; import { wrapInTag } from './keymaps/wrap'; -const validateCode = (editorInstance: Editor, code: string) => { +const validateCodeInEditor = (editorInstance: Editor, code: string) => { editorInstance.clearGutter('errorGutter'); - try { - compileJsx(code); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : ''; + const validOrError = validateCode(code); + if (validOrError !== true) { + const errorMessage = validOrError.message; const matches = errorMessage.match(/\(([0-9]+):/); const lineNumber = matches && matches.length >= 2 && matches[1] && parseInt(matches[1], 10); @@ -171,7 +170,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => { } editorInstanceRef.current.setValue(code); - validateCode(editorInstanceRef.current, code); + validateCodeInEditor(editorInstanceRef.current, code); } }, [code, previewCode]); @@ -212,12 +211,12 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => { { editorInstanceRef.current = editorInstance; - validateCode(editorInstance, code); + validateCodeInEditor(editorInstance, code); setCursorPosition(cursorPosition); }} onChange={(editorInstance, data, newCode) => { if (editorInstance.hasFocus() && !previewCode) { - validateCode(editorInstance, newCode); + validateCodeInEditor(editorInstance, newCode); debouncedChange(newCode); } }} diff --git a/src/Playroom/Frames/Frames.tsx b/src/Playroom/Frames/Frames.tsx index 79de0932..42f7e548 100644 --- a/src/Playroom/Frames/Frames.tsx +++ b/src/Playroom/Frames/Frames.tsx @@ -1,11 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import flatMap from 'lodash/flatMap'; import Iframe from './Iframe'; -import { - compileJsx, - openFragmentTag, - closeFragmentTag, -} from '../../utils/compileJsx'; +import { compileJsx } from '../../utils/compileJsx'; import type { PlayroomProps } from '../Playroom'; import { Strong } from '../Strong/Strong'; import { Text } from '../Text/Text'; @@ -22,6 +18,7 @@ interface FramesProps { export default function Frames({ code, themes, widths }: FramesProps) { const scrollingPanelRef = useRef(null); + const [renderCode, setRenderCode] = useState(''); const frames = flatMap(widths, (width) => themes.map((theme) => ({ @@ -31,14 +28,9 @@ export default function Frames({ code, themes, widths }: FramesProps) { })) ); - const [renderCode, setRenderCode] = useState( - () => `${openFragmentTag}${closeFragmentTag}` - ); - useEffect(() => { try { - const newCode = compileJsx(code); - setRenderCode(newCode); + setRenderCode(compileJsx(code)); } catch (e) {} }, [code]); diff --git a/src/utils/compileJsx.test.ts b/src/utils/compileJsx.test.ts new file mode 100644 index 00000000..6bce42b4 --- /dev/null +++ b/src/utils/compileJsx.test.ts @@ -0,0 +1,65 @@ +import dedent from 'dedent'; +import { compileJsx, validateCode } from './compileJsx'; + +describe('compileJsx', () => { + test('valid code', () => { + expect( + compileJsx(dedent` + + + `) + ).toMatchInlineSnapshot(` + "R_cE(R_F, null, R_cE(Foo, null ) + , R_cE(Bar, null ))" + `); + }); + + test('invalid code - no error', () => { + expect( + compileJsx(` + + `) + ).toMatchInlineSnapshot( + `"R_cE(R_F, null, R_cE(Foo--BarBaz ::invalid, null ))"` + ); + }); + + test('invalid code - with error', () => { + expect(() => + compileJsx(` + + + + `) + ).toThrowErrorMatchingInlineSnapshot(`"Unterminated JSX contents (3:25)"`); + }); +}); + +describe('validateCode', () => { + test('valid code', () => { + expect( + validateCode(` + + + `) + ).toBe(true); + }); + + test('invalid code', () => { + expect( + validateCode(` + + + + `) + ).toMatchInlineSnapshot(` + [SyntaxError: unknown: Unexpected token (4:20) + + 2 | + 3 | + > 4 | + | ^ + 5 | ] + `); + }); +}); diff --git a/src/utils/compileJsx.ts b/src/utils/compileJsx.ts index 167ed8ba..a48d4a92 100644 --- a/src/utils/compileJsx.ts +++ b/src/utils/compileJsx.ts @@ -1,4 +1,5 @@ import { transform } from 'sucrase'; +import { transform as transformWithBabel } from '@babel/standalone'; export const ReactFragmentPragma = 'R_F'; export const ReactCreateElementPragma = 'R_cE'; @@ -6,19 +7,37 @@ export const ReactCreateElementPragma = 'R_cE'; export const openFragmentTag = '<>'; export const closeFragmentTag = ''; +const wrapInFragment = (code: string) => + `${openFragmentTag}${code}${closeFragmentTag}`; + +// This one throws error with no useful information, but is fast export const compileJsx = (code: string) => - transform(`${openFragmentTag}${code.trim()}${closeFragmentTag}`, { + transform(wrapInFragment(code.trim()), { transforms: ['jsx'], jsxPragma: ReactCreateElementPragma, jsxFragmentPragma: ReactFragmentPragma, production: true, }).code; +// This one throws errors with line numbers. Useful for validation +const compileJsxWithBabel = (code: string) => + transformWithBabel(wrapInFragment(code), { + presets: [ + [ + 'react', + { + pragma: ReactCreateElementPragma, + pragmaFrag: ReactFragmentPragma, + }, + ], + ], + }); + export const validateCode = (code: string) => { try { - compileJsx(code); + compileJsxWithBabel(code); return true; } catch (err) { - return false; + return err as Error; } }; diff --git a/src/utils/cursor.ts b/src/utils/cursor.ts index cbfe50bb..8045f766 100644 --- a/src/utils/cursor.ts +++ b/src/utils/cursor.ts @@ -35,4 +35,4 @@ export const isValidLocation = ({ cursor, snippet: breakoutString, }) - ); + ) === true; From 082ec1822d065ffc08292edc78fe0e670c592fc8 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 11:41:41 +1100 Subject: [PATCH 04/10] throw error when scope overwrites our React pragmas --- src/Playroom/RenderCode/RenderCode.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Playroom/RenderCode/RenderCode.js b/src/Playroom/RenderCode/RenderCode.js index 5a753f50..8d5da3cf 100644 --- a/src/Playroom/RenderCode/RenderCode.js +++ b/src/Playroom/RenderCode/RenderCode.js @@ -10,9 +10,24 @@ import { } from '../../utils/compileJsx'; export default function RenderCode({ code, scope }) { - return scopeEval(code, { + const userScope = { ...(useScope() ?? {}), ...scope, + }; + + if (ReactCreateElementPragma in userScope) { + throw new Error( + `'${ReactCreateElementPragma}' is used internally by Playroom and is not allowed in scope` + ); + } + if (ReactFragmentPragma in userScope) { + throw new Error( + `'${ReactFragmentPragma}' is used internally by Playroom and is not allowed in scope` + ); + } + + return scopeEval(code, { + ...userScope, React, [ReactCreateElementPragma]: React.createElement, [ReactFragmentPragma]: React.Fragment, From fb3b21dbc692b4ada127af80c706068809df47f2 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 11:53:16 +1100 Subject: [PATCH 05/10] tweak Babel options to make it faster --- src/utils/compileJsx.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/compileJsx.ts b/src/utils/compileJsx.ts index a48d4a92..75f531a5 100644 --- a/src/utils/compileJsx.ts +++ b/src/utils/compileJsx.ts @@ -10,7 +10,7 @@ export const closeFragmentTag = ''; const wrapInFragment = (code: string) => `${openFragmentTag}${code}${closeFragmentTag}`; -// This one throws error with no useful information, but is fast +// This one sometimes throws errors with less useful information, but is fast export const compileJsx = (code: string) => transform(wrapInFragment(code.trim()), { transforms: ['jsx'], @@ -26,14 +26,18 @@ const compileJsxWithBabel = (code: string) => [ 'react', { + development: false, + pure: false, pragma: ReactCreateElementPragma, pragmaFrag: ReactFragmentPragma, + runtime: 'classic', + useBuiltIns: true, }, ], ], }); -export const validateCode = (code: string) => { +export const validateCode = (code: string): true | Error => { try { compileJsxWithBabel(code); return true; From 2dc6ab24cb7fab1cddac1ea8e7aefa9c7448fe48 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 11:54:05 +1100 Subject: [PATCH 06/10] add changesets --- .changeset/dry-suns-smash.md | 5 +++++ .changeset/light-baboons-glow.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/dry-suns-smash.md create mode 100644 .changeset/light-baboons-glow.md diff --git a/.changeset/dry-suns-smash.md b/.changeset/dry-suns-smash.md new file mode 100644 index 00000000..77addc3d --- /dev/null +++ b/.changeset/dry-suns-smash.md @@ -0,0 +1,5 @@ +--- +'playroom': patch +--- + +Highlight the correct error location when code has syntax errors diff --git a/.changeset/light-baboons-glow.md b/.changeset/light-baboons-glow.md new file mode 100644 index 00000000..3eb1ff6e --- /dev/null +++ b/.changeset/light-baboons-glow.md @@ -0,0 +1,5 @@ +--- +'playroom': minor +--- + +Use smaller React pragmas to reduce the payload sent to iframes From dfa0b75ea0db906590a59ece13d26fe0ce50afaa Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 12:25:02 +1100 Subject: [PATCH 07/10] memoize code compilation --- package.json | 1 + pnpm-lock.yaml | 6 ++++++ src/utils/compileJsx.ts | 22 +++++++++++++--------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 48a4f4df..d3f665aa 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "localforage": "^1.10.0", "lodash": "^4.17.21", "lz-string": "^1.4.4", + "memoize-one": "^6.0.0", "mini-css-extract-plugin": "^2.7.2", "parse-prop-types": "^0.3.0", "polished": "^4.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1c40aa2..858d9e69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,7 @@ specifiers: localforage: ^1.10.0 lodash: ^4.17.21 lz-string: ^1.4.4 + memoize-one: ^6.0.0 mini-css-extract-plugin: ^2.7.2 parse-prop-types: ^0.3.0 polished: ^4.2.2 @@ -112,6 +113,7 @@ dependencies: localforage: 1.10.0 lodash: 4.17.21 lz-string: 1.4.4 + memoize-one: 6.0.0 mini-css-extract-plugin: 2.7.2_webpack@5.75.0 parse-prop-types: 0.3.0_prop-types@15.8.1 polished: 4.2.2 @@ -7324,6 +7326,10 @@ packages: fs-monkey: 1.0.3 dev: false + /memoize-one/6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + /meow/6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} diff --git a/src/utils/compileJsx.ts b/src/utils/compileJsx.ts index 75f531a5..14a09ed9 100644 --- a/src/utils/compileJsx.ts +++ b/src/utils/compileJsx.ts @@ -1,5 +1,6 @@ import { transform } from 'sucrase'; import { transform as transformWithBabel } from '@babel/standalone'; +import memoizeOne from 'memoize-one'; export const ReactFragmentPragma = 'R_F'; export const ReactCreateElementPragma = 'R_cE'; @@ -11,16 +12,18 @@ const wrapInFragment = (code: string) => `${openFragmentTag}${code}${closeFragmentTag}`; // This one sometimes throws errors with less useful information, but is fast -export const compileJsx = (code: string) => - transform(wrapInFragment(code.trim()), { - transforms: ['jsx'], - jsxPragma: ReactCreateElementPragma, - jsxFragmentPragma: ReactFragmentPragma, - production: true, - }).code; +export const compileJsx = memoizeOne( + (code: string) => + transform(wrapInFragment(code.trim()), { + transforms: ['jsx'], + jsxPragma: ReactCreateElementPragma, + jsxFragmentPragma: ReactFragmentPragma, + production: true, + }).code +); // This one throws errors with line numbers. Useful for validation -const compileJsxWithBabel = (code: string) => +const compileJsxWithBabel = memoizeOne((code: string) => transformWithBabel(wrapInFragment(code), { presets: [ [ @@ -35,7 +38,8 @@ const compileJsxWithBabel = (code: string) => }, ], ], - }); + }) +); export const validateCode = (code: string): true | Error => { try { From 8b91070cd49f13f7c4d1574784ae9863d244fcf8 Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Tue, 21 Nov 2023 12:25:34 +1100 Subject: [PATCH 08/10] replace state with a ref --- src/Playroom/Frames/Frames.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Playroom/Frames/Frames.tsx b/src/Playroom/Frames/Frames.tsx index 42f7e548..5ce42a72 100644 --- a/src/Playroom/Frames/Frames.tsx +++ b/src/Playroom/Frames/Frames.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; import flatMap from 'lodash/flatMap'; import Iframe from './Iframe'; import { compileJsx } from '../../utils/compileJsx'; @@ -18,7 +18,7 @@ interface FramesProps { export default function Frames({ code, themes, widths }: FramesProps) { const scrollingPanelRef = useRef(null); - const [renderCode, setRenderCode] = useState(''); + const renderCode = useRef(''); const frames = flatMap(widths, (width) => themes.map((theme) => ({ @@ -28,11 +28,9 @@ export default function Frames({ code, themes, widths }: FramesProps) { })) ); - useEffect(() => { - try { - setRenderCode(compileJsx(code)); - } catch (e) {} - }, [code]); + try { + renderCode.current = compileJsx(code); + } catch (e) {} return (
@@ -46,7 +44,7 @@ export default function Frames({ code, themes, widths }: FramesProps) {