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)