From 69b4c7f3e8cd8571f25ede21fdfe68fa15f9d794 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:32:12 +0000 Subject: [PATCH] feat(editor) Support parsing components with top level if statements --- .../__snapshots__/ui-jsx-canvas.spec.tsx.snap | 4 +- .../components/canvas/ui-jsx-canvas.spec.tsx | 10 +- editor/src/core/shared/element-template.ts | 14 +++ .../parser-printer-code-preservation.spec.ts | 1 + .../parser-printer/parser-printer-parsing.ts | 69 ++++++++++++-- .../parser-printer-transpiling.ts | 16 +++- .../parser-printer/parser-printer-utils.ts | 19 ++++ .../parser-printer.test-utils.ts | 21 ----- .../workers/parser-printer/parser-printer.ts | 93 ++++++++++--------- 9 files changed, 167 insertions(+), 80 deletions(-) diff --git a/editor/src/components/canvas/__snapshots__/ui-jsx-canvas.spec.tsx.snap b/editor/src/components/canvas/__snapshots__/ui-jsx-canvas.spec.tsx.snap index 41ffa3e08065..20b1d5af68a5 100644 --- a/editor/src/components/canvas/__snapshots__/ui-jsx-canvas.spec.tsx.snap +++ b/editor/src/components/canvas/__snapshots__/ui-jsx-canvas.spec.tsx.snap @@ -46931,8 +46931,8 @@ exports[`UiJsxCanvas render renders fine with two components that reference each data-uid=\\"scene\\" >
great
diff --git a/editor/src/components/canvas/ui-jsx-canvas.spec.tsx b/editor/src/components/canvas/ui-jsx-canvas.spec.tsx index 3914959bbcdb..43b219bd9acf 100644 --- a/editor/src/components/canvas/ui-jsx-canvas.spec.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas.spec.tsx @@ -1565,18 +1565,20 @@ import Utopia, { } from 'utopia-api' export var A = (props) => { + // @utopia/uid=aaa-root if (props.x === 0) { - return
great
+ return
great
} else { - return + return } } export var B = (props) => { + // @utopia/uid=bbb-root if (props.x === 0) { - return
great
+ return
great
} else { - return + return } } diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index f93b17537a53..606091388845 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -1323,6 +1323,20 @@ export type JSXElementChild = | JSXFragment | JSXConditionalExpression +export function canBeRootElementOfComponent(element: JSXElementChild): boolean { + if (isJSXElement(element) || isJSXFragment(element) || isJSXConditionalExpression(element)) { + return true + } + + if (isJSExpression(element)) { + if (hasElementsWithin(element)) { + return Object.keys(element.elementsWithin).length > 0 + } + } + + return false +} + export function isJSXElement(element: JSXElementChild): element is JSXElement { return element.type === 'JSX_ELEMENT' } diff --git a/editor/src/core/workers/parser-printer/parser-printer-code-preservation.spec.ts b/editor/src/core/workers/parser-printer/parser-printer-code-preservation.spec.ts index 58a7422e42ce..38a65d1b9d58 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-code-preservation.spec.ts +++ b/editor/src/core/workers/parser-printer/parser-printer-code-preservation.spec.ts @@ -82,6 +82,7 @@ import { GithubPicker } from "react-color"; function Picker() { const [color, setColor] = useThemeContext(); const [visible, setVisible] = usePickerVisibilityContext(); + if (visible) { return { const { code: codeFromFile, map } = fileSourceNode.toStringWithSourceMap({ file: filename }) const rawMap = JSON.parse(map.toString()) - const transpileEither = transpileJavascriptFromCode( + const firstTraspileEither = transpileJavascriptFromCode( sourceFile.fileName, sourceFile.text, codeFromFile, @@ -1062,6 +1062,21 @@ export function parseAttributeOtherJavaScript( true, applySteganography, ) + const transpileEither = foldEither( + () => + transpileJavascriptFromCode( + sourceFile.fileName, + sourceFile.text, + codeFromFile, + rawMap, + parsedElementsWithin, + false, + applySteganography, + true, + ), + (success) => right(success), + firstTraspileEither, + ) return mapEither((transpileResult) => { const prependedWithReturn = prependToSourceString( sourceFile.fileName, @@ -3047,15 +3062,49 @@ export function parseOutFunctionContents( alreadyExistingUIDs, applySteganography, ) - return mapEither((parsed) => { - highlightBounds = mergeHighlightBounds(highlightBounds, parsed.highlightBounds) - return withParserMetadata( - functionContents(jsBlock, 'block', parsed.value, returnStatementComments), - highlightBounds, - propsUsed.concat(parsed.propsUsed), - definedElsewhere.concat(parsed.definedElsewhere), - ) - }, parsedElements) + return foldEither( + () => { + const parsedAsArbitrary = parseAttributeOtherJavaScript( + sourceFile, + sourceText, + filename, + imports, + topLevelNames, + propsObjectName, + possibleElement, + highlightBounds, + alreadyExistingUIDs, + applySteganography, + ) + + return bimapEither( + (failure) => failure, + (success) => { + highlightBounds = mergeHighlightBounds(highlightBounds, success.highlightBounds) + const elem = successfullyParsedElement(sourceFile, possibleElement, success.value) + return withParserMetadata( + functionContents(jsBlock, 'block', [elem], returnStatementComments), + highlightBounds, + propsUsed.concat(success.propsUsed), + definedElsewhere.concat(success.definedElsewhere), + ) + }, + parsedAsArbitrary, + ) + }, + (parsed) => { + highlightBounds = mergeHighlightBounds(highlightBounds, parsed.highlightBounds) + return right( + withParserMetadata( + functionContents(jsBlock, 'block', parsed.value, returnStatementComments), + highlightBounds, + propsUsed.concat(parsed.propsUsed), + definedElsewhere.concat(parsed.definedElsewhere), + ), + ) + }, + parsedElements, + ) } } else { const parsedElements = parseOutJSXElements( diff --git a/editor/src/core/workers/parser-printer/parser-printer-transpiling.ts b/editor/src/core/workers/parser-printer/parser-printer-transpiling.ts index 6a84ad320a64..4588a891e754 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-transpiling.ts +++ b/editor/src/core/workers/parser-printer/parser-printer-transpiling.ts @@ -16,7 +16,11 @@ import { fastForEach } from '../../shared/utils' import type { RawSourceMap } from '../ts/ts-typings/RawSourceMap' import infiniteLoopPrevention from './transform-prevent-infinite-loops' import type { ElementsWithinInPosition, CodeWithMap } from './parser-printer-utils' -import { wrapCodeInParens, wrapCodeInParensWithMap } from './parser-printer-utils' +import { + wrapCodeInAnonFunctionWithMap, + wrapCodeInParens, + wrapCodeInParensWithMap, +} from './parser-printer-utils' import { JSX_CANVAS_LOOKUP_FUNCTION_NAME } from '../../shared/dom-utils' import type { SteganoTextData } from '../../shared/stegano-text' import { cleanSteganoTextData, encodeSteganoData } from '../../shared/stegano-text' @@ -317,6 +321,7 @@ export function transpileJavascriptFromCode( elementsWithin: ElementsWithinInPosition, wrapInParens: boolean, applySteganography: SteganographyMode, + wrapInAnonFunction: boolean = false, ): Either { try { let codeToUse: string = code @@ -331,6 +336,15 @@ export function transpileJavascriptFromCode( ) codeToUse = wrappedInParens.code mapToUse = wrappedInParens.sourceMap + } else if (wrapInAnonFunction) { + const wrappedInAnonFunction = wrapCodeInAnonFunctionWithMap( + sourceFileName, + sourceFileText, + codeToUse, + mapToUse, + ) + codeToUse = wrappedInAnonFunction.code + mapToUse = wrappedInAnonFunction.sourceMap } let plugins: Array = diff --git a/editor/src/core/workers/parser-printer/parser-printer-utils.ts b/editor/src/core/workers/parser-printer/parser-printer-utils.ts index 2037f90e28eb..400ff0d1264a 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-utils.ts +++ b/editor/src/core/workers/parser-printer/parser-printer-utils.ts @@ -93,6 +93,25 @@ export function wrapCodeInParensWithMap( return { code: result.code, sourceMap: result.map } } +function wrapCodeInAnonFunction(code: string): string { + return `(() => {${removeTrailingSemicolon(code)}})()` +} + +export function wrapCodeInAnonFunctionWithMap( + sourceFileName: string, + sourceFileText: string, + code: string, + sourceMap: RawSourceMap, +): CodeWithMap { + const wrappedCode = wrapCodeInAnonFunction(code) + + const consumer = new SourceMapConsumer(sourceMap) + const node = SourceNode.fromStringWithSourceMap(wrappedCode, consumer) + node.setSourceContent(sourceFileName, sourceFileText) + const result = node.toStringWithSourceMap({ file: sourceFileName }) + return { code: result.code, sourceMap: result.map } +} + export function prependToSourceString( sourceFileName: string, sourceFileText: string, diff --git a/editor/src/core/workers/parser-printer/parser-printer.test-utils.ts b/editor/src/core/workers/parser-printer/parser-printer.test-utils.ts index 2b978b694a7a..23703ab0dbcd 100644 --- a/editor/src/core/workers/parser-printer/parser-printer.test-utils.ts +++ b/editor/src/core/workers/parser-printer/parser-printer.test-utils.ts @@ -217,7 +217,6 @@ export function testParseCode( fastForEach(success.topLevelElements, (topLevelElement) => { if (isUtopiaJSXComponent(topLevelElement)) { ensureElementsHaveUID(topLevelElement.rootElement, uids, () => true, 'walk-attributes') - ensureArbitraryJSXBlockCodeHasUIDs(topLevelElement.rootElement) } }) }, result) @@ -1196,26 +1195,6 @@ function babelCheckForDataUID(): { visitor: BabelTraverse.Visitor } { } } -export function ensureArbitraryJSXBlockCodeHasUIDs(jsxElementChild: JSXElementChild): void { - walkElements( - jsxElementChild, - 'do-not-include-data-uid-attribute', - (element) => { - if (isJSExpressionMapOrOtherJavaScript(element)) { - const plugins: Array = [ReactSyntaxPlugin, babelCheckForDataUID] - - Babel.transform(element.javascript, { - presets: [], - plugins: plugins, - sourceType: 'script', - }) - } - }, - () => true, - 'walk-attributes', - ) -} - export interface ArbitraryProject { code: string parsed: ParsedTextFile diff --git a/editor/src/core/workers/parser-printer/parser-printer.ts b/editor/src/core/workers/parser-printer/parser-printer.ts index 824f41f589df..c2a53d1f7a9a 100644 --- a/editor/src/core/workers/parser-printer/parser-printer.ts +++ b/editor/src/core/workers/parser-printer/parser-printer.ts @@ -31,6 +31,7 @@ import type { ImportStatement, ParsedComments, JSExpressionMapOrOtherJavascript, + JSExpressionOtherJavaScript, } from '../../shared/element-template' import { destructuredArray, @@ -51,6 +52,7 @@ import { isImportStatement, unparsedCode, emptyComments, + canBeRootElementOfComponent, } from '../../shared/element-template' import { messageisFatal } from '../../shared/error-messages' import { memoize } from '../../shared/memoize' @@ -649,10 +651,15 @@ function printUtopiaJSXComponent( TS.isJsxSelfClosingElement(asJSX) || TS.isJsxFragment(asJSX) || TS.isConditionalExpression(asJSX) || + TS.isJsxText(asJSX) || asJSX.kind === TS.SyntaxKind.NullKeyword ) { let elementNode: TS.Node - const jsxElementExpression = asJSX + const jsxElementExpression = TS.isJsxText(asJSX) + ? TS.createUnparsedSourceFile( + (element.rootElement as JSExpressionOtherJavaScript).originalJavascript, + ) + : asJSX const modifiers = getModifersForComponent(element, detailOfExports) const nodeFlags = element.declarationSyntax === 'function' @@ -670,7 +677,9 @@ function printUtopiaJSXComponent( statements.push(printArbitraryJSBlock(element.arbitraryJSBlock) as any) } - const returnStatement = TS.createReturn(jsxElementExpression) + const returnStatement = TS.isUnparsedSource(jsxElementExpression) + ? (jsxElementExpression as any) + : TS.createReturn(jsxElementExpression) addCommentsToNode(returnStatement, element.returnStatementComments) statements.push(returnStatement) return TS.createBlock(statements, printOptions.insertLinesBetweenStatements) @@ -680,11 +689,13 @@ function printUtopiaJSXComponent( if (element.arbitraryJSBlock != null || element.blockOrExpression === 'block') { return bodyForFunction() } else if (element.blockOrExpression === 'parenthesized-expression') { - const bodyExpression = TS.factory.createParenthesizedExpression(jsxElementExpression) + const bodyExpression = TS.factory.createParenthesizedExpression( + jsxElementExpression as any, // FIXME + ) addCommentsToNode(bodyExpression, element.returnStatementComments) return bodyExpression } else { - return jsxElementExpression + return jsxElementExpression as any // FIXME } } @@ -718,9 +729,18 @@ function printUtopiaJSXComponent( } } else { if (element.name == null) { - elementNode = TS.createExportAssignment(undefined, modifiers, undefined, asJSX) + elementNode = TS.createExportAssignment( + undefined, + modifiers, + undefined, + jsxElementExpression as any, // FIXME + ) } else { - const varDec = TS.createVariableDeclaration(element.name, undefined, asJSX) + const varDec = TS.createVariableDeclaration( + element.name, + undefined, + jsxElementExpression as any, // FIXME + ) const varDecList = TS.createVariableDeclarationList([varDec], nodeFlags) elementNode = TS.createVariableStatement(modifiers, varDecList) } @@ -1514,35 +1534,41 @@ export function parseCode( ), ) } - if (isLeft(parsedContents) || (isFunction && isLeft(parsedFunctionParam))) { - pushArbitraryNode(topLevelElement) - if (isExported(topLevelElement)) { - if (name == null) { + + // push any export details + if (isExported(topLevelElement)) { + if (name == null) { + detailOfExports = mergeExportsDetail(detailOfExports, [ + exportDefaultFunctionOrClass(null), + ]) + } else { + const defaultExport = markedAsDefault(topLevelElement) + if (defaultExport) { detailOfExports = mergeExportsDetail(detailOfExports, [ - exportDefaultFunctionOrClass(null), + exportDefaultFunctionOrClass(name), ]) } else { - const defaultExport = markedAsDefault(topLevelElement) - if (defaultExport) { - detailOfExports = mergeExportsDetail(detailOfExports, [ - exportDefaultFunctionOrClass(name), - ]) - } else { - detailOfExports = mergeExportsDetail(detailOfExports, [exportFunction(name)]) - } + detailOfExports = mergeExportsDetail(detailOfExports, [exportFunction(name)]) } } + } + + if (isLeft(parsedContents) || (isFunction && isLeft(parsedFunctionParam))) { + pushArbitraryNode(topLevelElement) } else { - highlightBounds = { - ...highlightBounds, - ...parsedContents.value.highlightBounds, - } const contents = parsedContents.value.value // If propsUsed is already populated, it's because the user used destructuring, so we can // use that. Otherwise, we have to use the list retrieved during parsing propsUsed = propsUsed.length > 0 ? propsUsed : uniq(parsedContents.value.propsUsed) - if (contents.elements.length === 1) { - const exported = isExported(topLevelElement) + if ( + contents.elements.length === 1 && + canBeRootElementOfComponent(contents.elements[0].value) + ) { + highlightBounds = { + ...highlightBounds, + ...parsedContents.value.highlightBounds, + } + // capture var vs let vs const vs function here const utopiaComponent = utopiaJSXComponent( name, @@ -1561,23 +1587,6 @@ export function parseCode( contents.returnStatementComments, ) - const defaultExport = markedAsDefault(topLevelElement) - if (exported) { - if (name == null) { - detailOfExports = mergeExportsDetail(detailOfExports, [ - exportDefaultFunctionOrClass(null), - ]) - } else { - if (defaultExport) { - detailOfExports = mergeExportsDetail(detailOfExports, [ - exportDefaultFunctionOrClass(name), - ]) - } else { - detailOfExports = mergeExportsDetail(detailOfExports, [exportFunction(name)]) - } - } - } - topLevelElements.push(right(utopiaComponent)) } else { pushArbitraryNode(topLevelElement)