From 28dadd18eaac35fa3be7331bf70ff761bd224a1b Mon Sep 17 00:00:00 2001 From: Remus Mate Date: Fri, 17 Nov 2023 11:44:35 +1100 Subject: [PATCH] WIP --- @types/esbuild.wasm.d.ts | 4 ++ lib/makeWebpackConfig.js | 7 ++++ package.json | 2 + pnpm-lock.yaml | 17 +++++++- src/Playroom/CodeEditor/CodeEditor.tsx | 18 ++++----- src/Playroom/Frames/Frames.tsx | 22 +++++------ src/Playroom/Preview.tsx | 16 ++++++-- src/Playroom/RenderCode/RenderCode.js | 8 ++++ src/utils/compileJsx.ts | 54 +++++++++++++++++++------- src/utils/cursor.ts | 2 +- 10 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 @types/esbuild.wasm.d.ts diff --git a/@types/esbuild.wasm.d.ts b/@types/esbuild.wasm.d.ts new file mode 100644 index 00000000..aeddc09c --- /dev/null +++ b/@types/esbuild.wasm.d.ts @@ -0,0 +1,4 @@ +declare module 'esbuild-wasm/esbuild.wasm' { + const wasmURL: string; + export default wasmURL; +} diff --git a/lib/makeWebpackConfig.js b/lib/makeWebpackConfig.js index 6ca8355b..abeb5bd1 100644 --- a/lib/makeWebpackConfig.js +++ b/lib/makeWebpackConfig.js @@ -141,6 +141,13 @@ module.exports = async (playroomConfig, options) => { include: path.dirname(require.resolve('codemirror/package.json')), use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')], }, + { + test: /esbuild.wasm$/, + type: 'asset/resource', + generator: { + filename: 'esbuild-[hash].wasm' + } + }, ], }, optimization: { diff --git a/package.json b/package.json index 9892cc1a..98982792 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "css-loader": "^6.7.2", "current-git-branch": "^1.1.0", "dedent": "^0.7.0", + "esbuild-wasm": "^0.19.5", "fast-glob": "^3.2.12", "find-up": "^5.0.0", "fuzzy": "^0.1.3", @@ -92,6 +93,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 a5a2dc05..931e8c46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,7 @@ specifiers: current-git-branch: ^1.1.0 cypress: ^12.0.2 dedent: ^0.7.0 + esbuild-wasm: ^0.19.5 eslint: ^8.44.0 eslint-config-seek: ^11.3.1 fast-glob: ^3.2.12 @@ -49,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 @@ -101,6 +103,7 @@ dependencies: css-loader: 6.7.2_webpack@5.75.0 current-git-branch: 1.1.0 dedent: 0.7.0 + esbuild-wasm: 0.19.5 fast-glob: 3.2.12 find-up: 5.0.0 fuzzy: 0.1.3 @@ -110,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 @@ -4784,6 +4788,12 @@ packages: is-symbol: 1.0.4 dev: true + /esbuild-wasm/0.19.5: + resolution: {integrity: sha512-7zmLLn2QCj93XfMmHtzrDJ1UBuOHB2CZz1ghoCEZiRajxjUvHsF40PnbzFIY/pmesqPRaEtEWii0uzsTbnAgrA==} + engines: {node: '>=12'} + hasBin: true + dev: false + /esbuild/0.11.23: resolution: {integrity: sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q==} hasBin: true @@ -6985,7 +6995,6 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true /jsonfile/4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -7158,7 +7167,7 @@ packages: dependencies: big.js: 5.2.2 emojis-list: 3.0.0 - json5: 2.2.1 + json5: 2.2.3 dev: false /localforage/1.10.0: @@ -7311,6 +7320,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/Playroom/CodeEditor/CodeEditor.tsx b/src/Playroom/CodeEditor/CodeEditor.tsx index a57ec53d..3df04653 100644 --- a/src/Playroom/CodeEditor/CodeEditor.tsx +++ b/src/Playroom/CodeEditor/CodeEditor.tsx @@ -12,8 +12,8 @@ import { import { formatCode as format, isMac } from '../../utils/formatting'; import { closeFragmentTag, - compileJsx, openFragmentTag, + validateCode, } from '../../utils/compileJsx'; import * as styles from './CodeEditor.css'; @@ -42,13 +42,13 @@ 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 instanceof Error ? validOrError.message : ''; const matches = errorMessage.match(/\(([0-9]+):/); const lineNumber = matches && matches.length >= 2 && matches[1] && parseInt(matches[1], 10); @@ -172,7 +172,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => { } editorInstanceRef.current.setValue(code); - validateCode(editorInstanceRef.current, code); + validateCodeInEditor(editorInstanceRef.current, code); } }, [code, previewCode]); @@ -213,12 +213,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..a24888e4 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'; @@ -31,16 +27,16 @@ export default function Frames({ code, themes, widths }: FramesProps) { })) ); - const [renderCode, setRenderCode] = useState( - () => `${openFragmentTag}${closeFragmentTag}` - ); + const [renderCode, setRenderCode] = useState(''); useEffect(() => { - try { - const newCode = compileJsx(code); - setRenderCode(newCode); - } catch (e) {} - }, [code]); + (async () => { + try { + const newCode = await compileJsx(code); + setRenderCode(newCode); + } catch (e) {} + })(); + }); return (
diff --git a/src/Playroom/Preview.tsx b/src/Playroom/Preview.tsx index 74a49240..fb207779 100644 --- a/src/Playroom/Preview.tsx +++ b/src/Playroom/Preview.tsx @@ -1,4 +1,4 @@ -import type { ComponentType, ReactNode } from 'react'; +import { useState, type ComponentType, type ReactNode, useEffect } from 'react'; import lzString from 'lz-string'; import { useParams } from '../utils/params'; @@ -32,13 +32,23 @@ export default ({ themes, components, FrameComponent }: PreviewProps) => { ); return { - code: compileJsx(result.code), + code: result.code, themeName: result.theme, }; } return {}; }); + const [renderCode, setRenderCode] = useState(''); + + useEffect(() => { + (async () => { + try { + const newCode = await compileJsx(code || ''); + setRenderCode(newCode); + } catch (e) {} + })(); + }); const resolvedTheme = themeName ? themes[themeName] : null; @@ -49,7 +59,7 @@ export default ({ themes, components, FrameComponent }: PreviewProps) => { themeName={themeName || '__PLAYROOM__NO_THEME__'} theme={resolvedTheme} > - +
diff --git a/src/Playroom/RenderCode/RenderCode.js b/src/Playroom/RenderCode/RenderCode.js index 5a753f50..e47fbebe 100644 --- a/src/Playroom/RenderCode/RenderCode.js +++ b/src/Playroom/RenderCode/RenderCode.js @@ -10,6 +10,14 @@ import { } from '../../utils/compileJsx'; export default function RenderCode({ code, scope }) { + // TODO: move these up + if (ReactCreateElementPragma in scope) { + throw new Error(`${ReactCreateElementPragma} not allowed in scope`); + } + if (ReactFragmentPragma in scope) { + throw new Error(`${ReactFragmentPragma} not allowed in scope`); + } + return scopeEval(code, { ...(useScope() ?? {}), ...scope, diff --git a/src/utils/compileJsx.ts b/src/utils/compileJsx.ts index f67da928..a5490bec 100644 --- a/src/utils/compileJsx.ts +++ b/src/utils/compileJsx.ts @@ -1,4 +1,11 @@ import { transform } from '@babel/standalone'; +import memoizeOne from 'memoize-one'; +import * as esbuild from 'esbuild-wasm'; +import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm'; + +const init = esbuild.initialize({ + wasmURL: `/${esbuildWasmUrl}`, +}); export const ReactFragmentPragma = 'R_F'; export const ReactCreateElementPragma = 'R_cE'; @@ -6,24 +13,43 @@ export const ReactCreateElementPragma = 'R_cE'; export const openFragmentTag = '<>'; export const closeFragmentTag = ''; -export const compileJsx = (code: string) => - transform(`${openFragmentTag}${code.trim()}${closeFragmentTag}`, { - presets: [ - [ - 'react', - { - pragma: ReactCreateElementPragma, - pragmaFrag: ReactFragmentPragma, - }, +export const compileJsxWithBabel = memoizeOne((code: string) => { + const result = transform( + `${openFragmentTag}${code.trim()}${closeFragmentTag}`, + { + presets: [ + [ + 'react', + { + pragma: ReactCreateElementPragma, + pragmaFrag: ReactFragmentPragma, + }, + ], ], - ], - }).code; + } + ); + return result.code; +}); + +export const compileJsx = memoizeOne(async (code: string) => { + await init; + const result = await esbuild.transform( + `${openFragmentTag}${code.trim()}${closeFragmentTag}`, + { + loader: 'jsx', + jsxFactory: ReactCreateElementPragma, + jsxFragment: ReactFragmentPragma, + } + ); + + return result.code; +}); -export const validateCode = (code: string) => { +export const validateCode = (code: string): true | Error => { 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;