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 b4707a65ac17..19c16a3d0eea 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 @@ -43525,6 +43525,1332 @@ export var App = (props) => { } `; +exports[`UiJsxCanvas render renders a canvas with a component wrapped in React.memo 1`] = ` +"
+
+
+
+ hi + +
+
+
+
+" +`; + +exports[`UiJsxCanvas render renders a canvas with a component wrapped in React.memo 2`] = ` +Object { + "utopia-storyboard-uid/scene-aaa": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [ + Object { + "children": Array [], + "name": Object { + "baseVariable": "App", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "app-entity", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": "100%", + "position": "absolute", + "width": "100%", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "title", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "Hi there!", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + ], + "name": Object { + "baseVariable": "Scene", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": 200, + "left": 59, + "position": "absolute", + "top": 79, + "width": 200, + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "scene-aaa", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "exportedName": "Scene", + "filePath": "utopia-api", + "type": "IMPORTED_ORIGIN", + "variableName": "Scene", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, + "utopia-storyboard-uid/scene-aaa/app-entity": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [], + "name": Object { + "baseVariable": "App", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "app-entity", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": "100%", + "position": "absolute", + "width": "100%", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "title", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "Hi there!", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + "app-entity", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "filePath": "test.js", + "type": "SAME_FILE_ORIGIN", + "variableName": "App", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, + "utopia-storyboard-uid/scene-aaa/app-entity:aaa": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "hi", + "type": "JSX_TEXT_BLOCK", + "uid": "", + }, + ], + "name": Object { + "baseVariable": "span", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "position": "absolute", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "bbb", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + ], + "name": Object { + "baseVariable": "div", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "content": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "SPREAD_ASSIGNMENT", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "onValue": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "name": "props", + "sourceMap": Object { + "mappings": "CAOyB", + "names": Array [], + "sources": Array [ + "code.tsx", + ], + "sourcesContent": Array [ + " + import * as React from 'react' + import { Storyboard, Scene } from 'utopia-api' + + export var App = React.memo((props) => { + return ( +
+ hi +
+ ) + }) + export var storyboard = (props) => { + return ( + + + + + + ) + } + ", + ], + "version": 3, + }, + "type": "JS_IDENTIFIER", + "uid": "", + }, + "optionallyChained": "not-optionally-chained", + "originalJavascript": "props.style", + "property": "style", + "sourceMap": Object { + "mappings": "CAOyB", + "names": Array [], + "sources": Array [ + "code.tsx", + ], + "sourcesContent": Array [ + " + import * as React from 'react' + import { Storyboard, Scene } from 'utopia-api' + + export var App = React.memo((props) => { + return ( +
+ hi +
+ ) + }) + export var storyboard = (props) => { + return ( + + + + + + ) + } + ", + ], + "version": 3, + }, + "type": "JS_PROPERTY_ACCESS", + "uid": "", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "backgroundColor", + "keyComments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "PROPERTY_ASSIGNMENT", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "#FFFFFF", + }, + }, + ], + "type": "ATTRIBUTE_NESTED_OBJECT", + "uid": "", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "aaa", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + "app-entity", + ], + Array [ + "aaa", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "filePath": "test.js", + "type": "SAME_FILE_ORIGIN", + "variableName": "div", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, + "utopia-storyboard-uid/scene-aaa/app-entity:aaa/bbb": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [ + Object { + "text": "hi", + "type": "JSX_TEXT_BLOCK", + "uid": "", + }, + ], + "name": Object { + "baseVariable": "span", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "position": "absolute", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "bbb", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + "app-entity", + ], + Array [ + "aaa", + "bbb", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "filePath": "test.js", + "type": "SAME_FILE_ORIGIN", + "variableName": "span", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, +} +`; + +exports[`UiJsxCanvas render renders a canvas with a component wrapped in a function that wraps the component with more content 1`] = ` +"
+
+
+
+
wrapComponent
+
+
+ hi +
+
+
+
+
+
+" +`; + +exports[`UiJsxCanvas render renders a canvas with a component wrapped in a function that wraps the component with more content 2`] = ` +Object { + "utopia-storyboard-uid/scene-aaa": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [ + Object { + "children": Array [], + "name": Object { + "baseVariable": "App", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "app-entity", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": "100%", + "position": "absolute", + "width": "100%", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "title", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "Hi there!", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + ], + "name": Object { + "baseVariable": "Scene", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": 200, + "left": 59, + "position": "absolute", + "top": 79, + "width": 200, + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "scene-aaa", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "exportedName": "Scene", + "filePath": "utopia-api", + "type": "IMPORTED_ORIGIN", + "variableName": "Scene", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, + "utopia-storyboard-uid/scene-aaa/app-entity": Object { + "assignedToProp": null, + "attributeMetadatada": Object {}, + "componentInstance": false, + "computedStyle": Object {}, + "conditionValue": "not-a-conditional", + "earlyReturn": null, + "element": Object { + "type": "RIGHT", + "value": Object { + "children": Array [], + "name": Object { + "baseVariable": "App", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "app-entity", + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "style", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": Object { + "height": "100%", + "position": "absolute", + "width": "100%", + }, + }, + }, + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "title", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "Hi there!", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + }, + "elementPath": Object { + "parts": Array [ + Array [ + "utopia-storyboard-uid", + "scene-aaa", + "app-entity", + ], + ], + "type": "elementpath", + }, + "globalFrame": null, + "importInfo": Object { + "filePath": "test.js", + "type": "SAME_FILE_ORIGIN", + "variableName": "App", + }, + "isEmotionOrStyledComponent": false, + "label": null, + "localFrame": null, + "nonRoundedGlobalFrame": null, + "specialSizeMeasurements": Object { + "alignItems": null, + "borderRadius": null, + "clientHeight": 0, + "clientWidth": 0, + "closestOffsetParentPath": Object { + "parts": Array [], + "type": "elementpath", + }, + "computedHugProperty": Object { + "height": null, + "width": null, + }, + "coordinateSystemBounds": null, + "display": "initial", + "flexDirection": null, + "float": "none", + "fontSize": null, + "fontStyle": null, + "fontWeight": null, + "gap": null, + "globalContentBoxForChildren": null, + "globalFrameWithTextContent": null, + "hasPositionOffset": false, + "hasTransform": false, + "htmlElementName": "div", + "immediateParentBounds": Object { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "immediateParentProvidesLayout": true, + "justifyContent": null, + "layoutSystemForChildren": null, + "margin": Object {}, + "naturalHeight": null, + "naturalWidth": null, + "offset": Object { + "x": 0, + "y": 0, + }, + "padding": Object {}, + "parentFlexDirection": null, + "parentFlexGap": 0, + "parentHugsOnMainAxis": false, + "parentJustifyContent": null, + "parentLayoutSystem": "flow", + "parentPadding": Object {}, + "parentTextDirection": "ltr", + "position": "static", + "providesBoundsForAbsoluteChildren": false, + "renderedChildrenCount": 0, + "textBounds": null, + "textDecorationLine": null, + "usesParentBounds": false, + }, + "textContent": null, + }, +} +`; + exports[`UiJsxCanvas render renders a component used in an arbitrary block correctly 1`] = ` "
) => { const colorTheme = useColorTheme() - const canvasIsLive = false const updateInvalidatedPaths = usePubSubAtomReadOnly( DomWalkerInvalidatePathsCtxAtom, AlwaysTrue, diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx index e23621d57f8c..e8e557494e6d 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils.tsx @@ -957,7 +957,7 @@ export function utopiaCanvasJSXLookup( } } -function runJSExpression( +export function runJSExpression( block: JSExpression, elementPath: ElementPath | null, currentScope: MapLike, diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx index f188450fd076..cd40fbbbf6f4 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx @@ -19,7 +19,12 @@ import { updateMutableUtopiaCtxRefWithNewProps, UtopiaProjectCtxAtom, } from './ui-jsx-canvas-contexts' -import { createLookupRender, utopiaCanvasJSXLookup } from './ui-jsx-canvas-element-renderer-utils' +import type { RenderContext } from './ui-jsx-canvas-element-renderer-utils' +import { + createLookupRender, + runJSExpression, + utopiaCanvasJSXLookup, +} from './ui-jsx-canvas-element-renderer-utils' import { runBlockUpdatingScope } from './ui-jsx-canvas-scope-utils' import * as EP from '../../../core/shared/element-path' import type { @@ -70,16 +75,6 @@ export function createExecutionScope( topLevelJsxComponents: Map requireResult: MapLike } { - const filePathMappings = getFilePathMappings(projectContents) - if (!(filePath in topLevelComponentRendererComponents.current)) { - // we make sure that the ref has an entry for this filepath - topLevelComponentRendererComponents.current[filePath] = {} - } - let topLevelComponentRendererComponentsForFile = - topLevelComponentRendererComponents.current[filePath] - - const fileBlobsForFile = defaultIfNull(emptyFileBlobs, fileBlobs[filePath]) - const { topLevelElements, imports, jsxFactoryFunction, combinedTopLevelArbitraryBlock } = getParseSuccessForFilePath(filePath, projectContents) const requireResult: MapLike = importResultFromImports(filePath, imports, customRequire) @@ -105,57 +100,97 @@ export function createExecutionScope( process: process, ...requireResult, } + const filePathMappings = getFilePathMappings(projectContents) + if (!(filePath in topLevelComponentRendererComponents.current)) { + // we make sure that the ref has an entry for this filepath + topLevelComponentRendererComponents.current[filePath] = {} + } + let topLevelComponentRendererComponentsForFile = + topLevelComponentRendererComponents.current[filePath] + + function addComponentToScope(component: UtopiaJSXComponent): void { + const elementName = component.name ?? 'default' + topLevelJsxComponents.set(component.name, component) + if (elementName in topLevelComponentRendererComponentsForFile) { + executionScope[elementName] = topLevelComponentRendererComponentsForFile[elementName] + } else { + const baseComponent = createComponentRendererComponent({ + topLevelElementName: component.name, + mutableContextRef: mutableContextRef, + filePath: filePath, + }) + const wrappedComponent = component.functionWrapping.reduceRight((workingComponent, wrap) => { + switch (wrap.type) { + case 'SIMPLE_FUNCTION_WRAP': + const functionExpression = runJSExpression( + wrap.functionExpression, + null, + executionScope, + renderContext, + wrap.functionExpression.uid, + null, + null, + ) + return functionExpression(workingComponent) + } + }, baseComponent) + topLevelComponentRendererComponentsForFile[elementName] = wrappedComponent + executionScope[elementName] = wrappedComponent + } + } + + const fileBlobsForFile = defaultIfNull(emptyFileBlobs, fileBlobs[filePath]) + // TODO All of this is run on every interaction o_O let topLevelJsxComponents: Map = new Map() - // Make sure there is something in scope for all of the top level components - fastForEach(topLevelElements, (topLevelElement) => { - if (isUtopiaJSXComponent(topLevelElement)) { - topLevelJsxComponents.set(topLevelElement.name, topLevelElement) - const elementName = topLevelElement.name ?? 'default' - if (!(elementName in topLevelComponentRendererComponentsForFile)) { - topLevelComponentRendererComponentsForFile[elementName] = createComponentRendererComponent({ - topLevelElementName: topLevelElement.name, - mutableContextRef: mutableContextRef, - filePath: filePath, - }) - } - } - }) - - executionScope = { - ...executionScope, - ...topLevelComponentRendererComponentsForFile, + const renderContext: RenderContext = { + rootScope: executionScope, + parentComponentInputProps: {}, + requireResult: requireResult, + hiddenInstances: hiddenInstances, + displayNoneInstances: displayNoneInstances, + fileBlobs: fileBlobsForFile, + validPaths: new Set(), + reactChildren: undefined, + metadataContext: metadataContext, + updateInvalidatedPaths: updateInvalidatedPaths, + jsxFactoryFunctionName: jsxFactoryFunction, + shouldIncludeCanvasRootInTheSpy: shouldIncludeCanvasRootInTheSpy, + filePath: filePath, + imports: imports, + code: '', + highlightBounds: {}, + editedText: editedText, + variablesInScope: {}, + filePathMappings: filePathMappings, } let spiedVariablesInRoot: VariableData = {} // First make sure everything is in scope if (combinedTopLevelArbitraryBlock != null && openStoryboardFileNameKILLME != null) { + for (const definedElsewhereValue of combinedTopLevelArbitraryBlock.definedElsewhere) { + const componentFromThisFile = topLevelElements.find( + (topLevelElement): topLevelElement is UtopiaJSXComponent => { + return ( + isUtopiaJSXComponent(topLevelElement) && topLevelElement.name === definedElsewhereValue + ) + }, + ) + if (componentFromThisFile != null) { + addComponentToScope(componentFromThisFile) + } + } const { highlightBounds, code } = getCodeAndHighlightBoundsForFile(filePath, projectContents) const propertiesFromParams = propertiesExposedByParams(combinedTopLevelArbitraryBlock.params) const lookupRenderer = createLookupRender( EP.emptyElementPath, { - rootScope: executionScope, - parentComponentInputProps: {}, - requireResult: requireResult, - hiddenInstances: hiddenInstances, - displayNoneInstances: displayNoneInstances, - fileBlobs: fileBlobsForFile, - validPaths: new Set(), - reactChildren: undefined, - metadataContext: metadataContext, - updateInvalidatedPaths: updateInvalidatedPaths, - jsxFactoryFunctionName: jsxFactoryFunction, - shouldIncludeCanvasRootInTheSpy: shouldIncludeCanvasRootInTheSpy, - filePath: openStoryboardFileNameKILLME, - imports: imports, + ...renderContext, code: code, highlightBounds: highlightBounds, - editedText: editedText, - variablesInScope: {}, filePathMappings: filePathMappings, }, null, @@ -179,6 +214,14 @@ export function createExecutionScope( ) spiedVariablesInRoot = spiedVariablesDeclaredWithinBlock } + + // Make sure there is something in scope for all of the top level components + fastForEach(topLevelElements, (topLevelElement) => { + if (isUtopiaJSXComponent(topLevelElement)) { + addComponentToScope(topLevelElement) + } + }) + // WARNING: mutating the mutableContextRef updateMutableUtopiaCtxRefWithNewProps(mutableContextRef, { ...mutableContextRef.current, diff --git a/editor/src/components/canvas/ui-jsx-canvas.spec.tsx b/editor/src/components/canvas/ui-jsx-canvas.spec.tsx index 2c9c1c937373..aac365459d04 100644 --- a/editor/src/components/canvas/ui-jsx-canvas.spec.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas.spec.tsx @@ -156,6 +156,91 @@ export { ToBeDefaultExported as default }`, ) }) + it('renders a canvas with a component wrapped in React.memo', () => { + testCanvasRender( + null, + ` + import * as React from 'react' + import { Storyboard, Scene } from 'utopia-api' + + export var App = React.memo((props) => { + return ( +
+ hi +
+ ) + }) + export var ${BakedInStoryboardVariableName} = (props) => { + return ( + + + + + + ) + } + `, + ) + }) + + it('renders a canvas with a component wrapped in a function that wraps the component with more content', () => { + testCanvasRender( + null, + ` + import * as React from 'react' + import { Storyboard, Scene } from 'utopia-api' + + function wrapComponent(Component) { + return () => { + return
+
+ wrapComponent +
+
+
+ } + } + + export var App = wrapComponent(() => { + return ( +
+ hi +
+ ) + }) + export var ${BakedInStoryboardVariableName} = (props) => { + return ( + + + + + + ) + } + `, + ) + }) + it('renders a canvas defined by a utopia storyboard component', () => { testCanvasRender( null, diff --git a/editor/src/components/editor/actions/actions.spec.tsx b/editor/src/components/editor/actions/actions.spec.tsx index 5bb199009b99..23e68963d126 100644 --- a/editor/src/components/editor/actions/actions.spec.tsx +++ b/editor/src/components/editor/actions/actions.spec.tsx @@ -154,6 +154,7 @@ function storyboardComponent(numberOfScenes: number): UtopiaJSXComponent { false, 'var', 'block', + [], null, [], jsxElement( @@ -186,6 +187,7 @@ const originalModel = deepFreeze( true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( diff --git a/editor/src/components/editor/store/store-deep-equality-instances-2.spec.ts b/editor/src/components/editor/store/store-deep-equality-instances-2.spec.ts index dab15fd7e549..428d06f16905 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances-2.spec.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances-2.spec.ts @@ -788,6 +788,7 @@ describe('UtopiaJSXComponentKeepDeepEquality', () => { isFunction: true, declarationSyntax: 'const', blockOrExpression: 'block', + functionWrapping: [], param: { type: 'PARAM', dotDotDotToken: false, @@ -837,6 +838,7 @@ describe('UtopiaJSXComponentKeepDeepEquality', () => { isFunction: true, declarationSyntax: 'const', blockOrExpression: 'block', + functionWrapping: [], param: { type: 'PARAM', dotDotDotToken: false, @@ -886,6 +888,7 @@ describe('UtopiaJSXComponentKeepDeepEquality', () => { isFunction: true, declarationSyntax: 'const', blockOrExpression: 'block', + functionWrapping: [], param: { type: 'PARAM', dotDotDotToken: false, diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 90c52ff49778..c2a87be51c3f 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -604,6 +604,8 @@ import type { OnlineState } from '../online-status' import { onlineState } from '../online-status' import type { NavigatorRow } from '../../navigator/navigator-row' import { condensedNavigatorRow, regularNavigatorRow } from '../../navigator/navigator-row' +import type { SimpleFunctionWrap, FunctionWrap } from 'utopia-shared/src/types' +import { simpleFunctionWrap, isSimpleFunctionWrap } from 'utopia-shared/src/types' export function ElementPropertyPathKeepDeepEquality(): KeepDeepEqualityCall { return combine2EqualityCalls( @@ -1570,8 +1572,29 @@ export function JSXElementChildKeepDeepEquality(): KeepDeepEqualityCall = + combine1EqualityCall( + (wrap) => wrap.functionExpression, + JSExpressionKeepDeepEqualityCall, + simpleFunctionWrap, + ) + +export const FunctionWrapKeepDeepEquality: KeepDeepEqualityCall = ( + oldValue, + newValue, +) => { + switch (oldValue.type) { + case 'SIMPLE_FUNCTION_WRAP': + if (isSimpleFunctionWrap(newValue)) { + return SimpleFunctionWrapKeepDeepEquality(oldValue, newValue) + } + break + } + return keepDeepEqualityResult(newValue, false) +} + export const UtopiaJSXComponentKeepDeepEquality: KeepDeepEqualityCall = - combine10EqualityCalls( + combine11EqualityCalls( (component) => component.name, createCallWithTripleEquals(), (component) => component.isFunction, @@ -1580,6 +1603,8 @@ export const UtopiaJSXComponentKeepDeepEquality: KeepDeepEqualityCall component.blockOrExpression, createCallWithTripleEquals(), + (component) => component.functionWrapping, + arrayDeepEquality(FunctionWrapKeepDeepEquality), (component) => component.param, nullableDeepEquality(ParamKeepDeepEquality()), (component) => component.propsUsed, diff --git a/editor/src/core/model/__snapshots__/storyboard-utils.spec.ts.snap b/editor/src/core/model/__snapshots__/storyboard-utils.spec.ts.snap index 9d067e4dce25..e1545668b90d 100644 --- a/editor/src/core/model/__snapshots__/storyboard-utils.spec.ts.snap +++ b/editor/src/core/model/__snapshots__/storyboard-utils.spec.ts.snap @@ -12,6 +12,7 @@ Array [ "arbitraryJSBlock": null, "blockOrExpression": "block", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": false, "name": "storyboard", "param": null, diff --git a/editor/src/core/model/element-template-utils.spec.tsx b/editor/src/core/model/element-template-utils.spec.tsx index a7e46db78aa7..b37102c9fdeb 100644 --- a/editor/src/core/model/element-template-utils.spec.tsx +++ b/editor/src/core/model/element-template-utils.spec.tsx @@ -192,6 +192,7 @@ describe('removeJSXElementChild', () => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -212,6 +213,7 @@ describe('removeJSXElementChild', () => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( diff --git a/editor/src/core/model/scene-utils.ts b/editor/src/core/model/scene-utils.ts index b10dacf9193a..fbbe93583339 100644 --- a/editor/src/core/model/scene-utils.ts +++ b/editor/src/core/model/scene-utils.ts @@ -116,6 +116,7 @@ export function convertScenesToUtopiaCanvasComponent( false, 'var', 'block', + [], null, [], jsxElement( diff --git a/editor/src/core/model/storyboard-utils.ts b/editor/src/core/model/storyboard-utils.ts index 46211943d6d7..c8bf0942475e 100644 --- a/editor/src/core/model/storyboard-utils.ts +++ b/editor/src/core/model/storyboard-utils.ts @@ -384,6 +384,7 @@ function addStoryboardFileForComponent( false, 'var', 'block', + [], null, [], storyboardElement, diff --git a/editor/src/core/model/test-ui-js-file.test-utils.ts b/editor/src/core/model/test-ui-js-file.test-utils.ts index 6829a626dc91..9bdb51c0b620 100644 --- a/editor/src/core/model/test-ui-js-file.test-utils.ts +++ b/editor/src/core/model/test-ui-js-file.test-utils.ts @@ -61,6 +61,7 @@ const mainComponentForTests = utopiaJSXComponent( true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -332,6 +333,7 @@ const scene = utopiaJSXComponent( true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -398,6 +400,7 @@ const TestStoryboard = utopiaJSXComponent( false, 'var', 'block', + [], null, [], jsxElement( diff --git a/editor/src/core/property-controls/property-controls-local.ts b/editor/src/core/property-controls/property-controls-local.ts index f688a5f9378b..340f7cf073b3 100644 --- a/editor/src/core/property-controls/property-controls-local.ts +++ b/editor/src/core/property-controls/property-controls-local.ts @@ -117,6 +117,7 @@ import type { ScriptLine } from '../../third-party/react-error-overlay/utils/sta import { intrinsicHTMLElementNamesAsStrings } from '../shared/dom-utils' import { valueOrArrayToArray } from '../shared/array-utils' import { optionalMap } from '../shared/optional-utils' +import type { RenderContext } from 'src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils' const exportedNameSymbol = Symbol('__utopia__exportedName') const moduleNameSymbol = Symbol('__utopia__moduleName') diff --git a/editor/src/core/shared/element-template.ts b/editor/src/core/shared/element-template.ts index af6d66b0fc11..c52695b3d1bd 100644 --- a/editor/src/core/shared/element-template.ts +++ b/editor/src/core/shared/element-template.ts @@ -111,6 +111,7 @@ import type { HugProperty, HugPropertyWidthHeight, ElementsByUID, + FunctionWrap, } from 'utopia-shared/src/types/element-template' import type { VariableData } from '../../components/canvas/ui-jsx-canvas' @@ -198,6 +199,7 @@ import { emptyComments, emptyComputedStyle, emptyAttributeMetadata, + simpleFunctionWrap, } from 'utopia-shared/src/types/element-template' export { emptyComments, emptyComputedStyle, emptyAttributeMetadata } @@ -1981,6 +1983,7 @@ export function utopiaJSXComponent( isFunction: boolean, declarationSyntax: FunctionDeclarationSyntax, blockOrExpression: BlockOrExpression, + functionWrapping: Array, param: Param | null, propsUsed: Array, rootElement: JSXElementChild, @@ -1994,6 +1997,7 @@ export function utopiaJSXComponent( isFunction: isFunction, declarationSyntax: declarationSyntax, blockOrExpression: blockOrExpression, + functionWrapping: functionWrapping, param: param, propsUsed: propsUsed, rootElement: rootElement, @@ -2179,8 +2183,6 @@ export function propertiesExposedByParam(param: Param): Array { } } -// FIXME we need to inject data-uids using insertDataUIDsIntoCode - export function clearArbitraryJSBlockUniqueIDs(block: ArbitraryJSBlock): ArbitraryJSBlock { return { ...block, @@ -2302,6 +2304,24 @@ export function clearParamUniqueIDs(param: Param): Param { return functionParam(param.dotDotDotToken, clearBoundParamUniqueIDs(param.boundParam)) } +export function clearFunctionWrapUniqueIDs(wrap: FunctionWrap): FunctionWrap { + switch (wrap.type) { + case 'SIMPLE_FUNCTION_WRAP': + return simpleFunctionWrap(clearExpressionUniqueIDs(wrap.functionExpression)) + } +} + +export function clearFunctionWrapSourceMaps(wrap: FunctionWrap): FunctionWrap { + switch (wrap.type) { + case 'SIMPLE_FUNCTION_WRAP': + return simpleFunctionWrap(clearExpressionSourceMaps(wrap.functionExpression)) + } +} + +export function clearFunctionWrapUniqueIDsAndSourceMaps(wrap: FunctionWrap): FunctionWrap { + return clearFunctionWrapSourceMaps(clearFunctionWrapUniqueIDs(wrap)) +} + // FIXME: Should only really be in test code. export function clearTopLevelElementUniqueIDs(element: UtopiaJSXComponent): UtopiaJSXComponent export function clearTopLevelElementUniqueIDs(element: ArbitraryJSBlock): ArbitraryJSBlock @@ -2312,6 +2332,7 @@ export function clearTopLevelElementUniqueIDs(element: TopLevelElement): TopLeve let updatedComponent: UtopiaJSXComponent = { ...element, rootElement: clearJSXElementChildUniqueIDs(element.rootElement), + functionWrapping: element.functionWrapping.map(clearFunctionWrapUniqueIDsAndSourceMaps), } if (updatedComponent.arbitraryJSBlock != null) { updatedComponent.arbitraryJSBlock = clearArbitraryJSBlockUniqueIDs( diff --git a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-arbitrary-elements.spec.tsx.snap b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-arbitrary-elements.spec.tsx.snap index 07cd2a31dd54..6c9a32b345b4 100644 --- a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-arbitrary-elements.spec.tsx.snap +++ b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-arbitrary-elements.spec.tsx.snap @@ -603,6 +603,7 @@ export var storyboard = ( "arbitraryJSBlock": null, "blockOrExpression": "block", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": true, "name": "App", "param": Object { @@ -987,6 +988,7 @@ export var storyboard = ( "arbitraryJSBlock": null, "blockOrExpression": "parenthesized-expression", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": false, "name": "storyboard", "param": null, diff --git a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-dot-notation.spec.tsx.snap b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-dot-notation.spec.tsx.snap index f89abc4ca6a5..02d5c59eea75 100644 --- a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-dot-notation.spec.tsx.snap +++ b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer-dot-notation.spec.tsx.snap @@ -48,6 +48,7 @@ Array [ "arbitraryJSBlock": null, "blockOrExpression": "block", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": true, "name": "App", "param": Object { diff --git a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer.spec.ts.snap b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer.spec.ts.snap index eeae8f05557a..24d0718487bd 100644 --- a/editor/src/core/workers/parser-printer/__snapshots__/parser-printer.spec.ts.snap +++ b/editor/src/core/workers/parser-printer/__snapshots__/parser-printer.spec.ts.snap @@ -566,6 +566,7 @@ export var whatever = (props) => { "arbitraryJSBlock": null, "blockOrExpression": "block", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": true, "name": "whatever", "param": Object { @@ -952,6 +953,7 @@ Object { "arbitraryJSBlock": null, "blockOrExpression": "expression", "declarationSyntax": "const", + "functionWrapping": Array [], "isFunction": true, "name": "a", "param": Object { @@ -1108,6 +1110,7 @@ const b = (n) => n > 0 ?
n
: a(10) "arbitraryJSBlock": null, "blockOrExpression": "expression", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": true, "name": "whatever", "param": Object { @@ -1166,6 +1169,7 @@ const b = (n) => n > 0 ?
n
: a(10) "arbitraryJSBlock": null, "blockOrExpression": "expression", "declarationSyntax": "const", + "functionWrapping": Array [], "isFunction": true, "name": "b", "param": Object { @@ -1656,6 +1660,7 @@ export var App = (props) => "arbitraryJSBlock": null, "blockOrExpression": "expression", "declarationSyntax": "var", + "functionWrapping": Array [], "isFunction": true, "name": "App", "param": Object { diff --git a/editor/src/core/workers/parser-printer/parser-printer-arbitrary-elements.spec.tsx b/editor/src/core/workers/parser-printer/parser-printer-arbitrary-elements.spec.tsx index 46df412fa758..6392ee2f2409 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-arbitrary-elements.spec.tsx +++ b/editor/src/core/workers/parser-printer/parser-printer-arbitrary-elements.spec.tsx @@ -320,6 +320,7 @@ export var whatever = props => ( true, 'var', 'expression', + [], defaultPropsParam, [], jsxElement( @@ -367,6 +368,7 @@ export var whatever = props => ( true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], view, @@ -507,6 +509,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -670,6 +673,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -817,6 +821,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -901,6 +906,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -1013,6 +1019,7 @@ return { arr: arr };` true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -1097,6 +1104,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -1207,6 +1215,7 @@ return { arr: arr };` true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -1466,6 +1475,7 @@ export var storyboard = ( true, 'var', 'block', + [], defaultPropsParam, [], rootElement, diff --git a/editor/src/core/workers/parser-printer/parser-printer-function-wrapped.spec.tsx b/editor/src/core/workers/parser-printer/parser-printer-function-wrapped.spec.tsx new file mode 100644 index 000000000000..52ce6f129b36 --- /dev/null +++ b/editor/src/core/workers/parser-printer/parser-printer-function-wrapped.spec.tsx @@ -0,0 +1,163 @@ +import { + isArbitraryJSBlock, + type ArbitraryJSBlock, + type UtopiaJSXComponent, + isUtopiaJSXComponent, +} from '../../shared/element-template' +import { isParseSuccess } from '../../shared/project-file-types' +import { printCode, printCodeOptions } from './parser-printer' +import { + clearParseResultSourceMapsUniqueIDsAndEmptyBlocks, + clearParseResultUniqueIDsAndEmptyBlocks, + testParseCode, +} from './parser-printer.test-utils' + +function getLoneMainTopLevelElement(code: string): UtopiaJSXComponent | ArbitraryJSBlock { + const parseResult = clearParseResultSourceMapsUniqueIDsAndEmptyBlocks(testParseCode(code)) + if (isParseSuccess(parseResult)) { + const topLevelElements = parseResult.topLevelElements + const mainTopLevelElements = topLevelElements.filter( + (element): element is UtopiaJSXComponent | ArbitraryJSBlock => + isUtopiaJSXComponent(element) || isArbitraryJSBlock(element), + ) + switch (mainTopLevelElements.length) { + case 0: + throw new Error(`No main top level elements found.`) + case 1: + return mainTopLevelElements[0] + default: + throw new Error(`More than one main top level element found: ${topLevelElements}`) + } + } else { + throw new Error(`Not a successful parse: ${parseResult}`) + } +} + +function testParsePrintParse(code: string): string { + const firstParse = clearParseResultUniqueIDsAndEmptyBlocks(testParseCode(code)) + + if (!isParseSuccess(firstParse)) { + throw new Error(JSON.stringify(firstParse)) + } + + const firstAsParseSuccess = firstParse + + const printed = printCode( + '/index.js', + printCodeOptions(false, true, true), + firstAsParseSuccess.imports, + firstAsParseSuccess.topLevelElements, + firstAsParseSuccess.jsxFactoryFunction, + firstAsParseSuccess.exportsDetail, + ) + + const secondParse = clearParseResultUniqueIDsAndEmptyBlocks(testParseCode(printed)) + + if (!isParseSuccess(secondParse)) { + throw new Error(JSON.stringify(secondParse)) + } + + const secondAsParseSuccess = firstParse + expect(secondAsParseSuccess.topLevelElements).toEqual(firstAsParseSuccess.topLevelElements) + + return printed +} + +const simpleReactMemoComponentCode = `import React from "react"; +export const Wrapped = React.memo(() => { + return
+})` + +describe('parseCode', () => { + it('simple function component wrapped in a React.memo', () => { + const element = getLoneMainTopLevelElement(simpleReactMemoComponentCode) + expect(element.type).toEqual('UTOPIA_JSX_COMPONENT') + expect(element).toMatchInlineSnapshot(` + Object { + "arbitraryJSBlock": null, + "blockOrExpression": "block", + "declarationSyntax": "const", + "functionWrapping": Array [ + Object { + "functionExpression": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "onValue": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "name": "React", + "sourceMap": null, + "type": "JS_IDENTIFIER", + "uid": "", + }, + "optionallyChained": "not-optionally-chained", + "originalJavascript": "React.memo", + "property": "memo", + "sourceMap": null, + "type": "JS_PROPERTY_ACCESS", + "uid": "", + }, + "type": "SIMPLE_FUNCTION_WRAP", + }, + ], + "isFunction": true, + "name": "Wrapped", + "param": null, + "propsUsed": Array [], + "returnStatementComments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "rootElement": Object { + "children": Array [], + "name": Object { + "baseVariable": "div", + "propertyPath": Object { + "propertyElements": Array [], + }, + }, + "props": Array [ + Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "key": "data-uid", + "type": "JSX_ATTRIBUTES_ENTRY", + "value": Object { + "comments": Object { + "leadingComments": Array [], + "trailingComments": Array [], + }, + "type": "ATTRIBUTE_VALUE", + "uid": "", + "value": "wrapped", + }, + }, + ], + "type": "JSX_ELEMENT", + "uid": "", + }, + "type": "UTOPIA_JSX_COMPONENT", + "usedInReactDOMRender": false, + } + `) + }) +}) + +describe('printCode', () => { + it('simple function component wrapped in a React.memo', () => { + const actualResult = testParsePrintParse(simpleReactMemoComponentCode) + expect(actualResult).toMatchInlineSnapshot(` + "import React from 'react' + export const Wrapped = React.memo(() => { + return
+ }) + " + `) + }) +}) diff --git a/editor/src/core/workers/parser-printer/parser-printer-functional-components.spec.ts b/editor/src/core/workers/parser-printer/parser-printer-functional-components.spec.ts index 7d09434dd43d..cfc9a8836b59 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-functional-components.spec.ts +++ b/editor/src/core/workers/parser-printer/parser-printer-functional-components.spec.ts @@ -196,6 +196,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -251,6 +252,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, [], view, @@ -289,6 +291,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, [], view, @@ -331,6 +334,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, ['prop'], view, @@ -388,6 +392,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, ['prop'], view, @@ -431,6 +436,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, ['prop'], view, @@ -488,6 +494,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, ['prop'], view, @@ -532,6 +539,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, ['prop'], view, @@ -571,6 +579,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, [], view, @@ -625,6 +634,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, [], view, @@ -668,6 +678,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], propsParam, [], view, @@ -724,6 +735,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], outerDestructuredObject, ['arrayPart'], view, @@ -812,6 +824,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], outerDestructuredObject, ['arrayPart'], view, @@ -851,6 +864,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -894,6 +908,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -950,6 +965,7 @@ describe('Parsing a function component with props', () => { true, 'var', 'block', + [], defaultPropsParam, ['showA'], expect.objectContaining({ diff --git a/editor/src/core/workers/parser-printer/parser-printer-uids.spec.tsx b/editor/src/core/workers/parser-printer/parser-printer-uids.spec.tsx index 926750fd95f3..e1835f8b11b9 100644 --- a/editor/src/core/workers/parser-printer/parser-printer-uids.spec.tsx +++ b/editor/src/core/workers/parser-printer/parser-printer-uids.spec.tsx @@ -410,6 +410,7 @@ export var app = (props) => { tle.isFunction, tle.declarationSyntax, tle.blockOrExpression, + tle.functionWrapping, tle.param, tle.propsUsed, updatedRootElement, diff --git a/editor/src/core/workers/parser-printer/parser-printer.spec.ts b/editor/src/core/workers/parser-printer/parser-printer.spec.ts index a590ad8db935..97b5d7a120b7 100644 --- a/editor/src/core/workers/parser-printer/parser-printer.spec.ts +++ b/editor/src/core/workers/parser-printer/parser-printer.spec.ts @@ -165,6 +165,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -230,6 +231,7 @@ export var whatever = () => true, 'var', 'expression', + [], null, [], view, @@ -323,6 +325,7 @@ export function whatever(props) { true, 'function', 'block', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -392,6 +395,7 @@ export function whatever() { true, 'function', 'block', + [], null, [], view, @@ -485,6 +489,7 @@ export default function whatever(props) { true, 'function', 'block', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -554,6 +559,7 @@ export default function whatever() { true, 'function', 'block', + [], null, [], view, @@ -644,6 +650,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -774,6 +781,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -863,6 +871,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -987,6 +996,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake', 'rightOfTheCake'], view, @@ -1085,6 +1095,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -1183,6 +1194,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -1373,6 +1385,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -1524,6 +1537,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -1684,6 +1698,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['leftOfTheCake'], view, @@ -1774,6 +1789,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -1951,6 +1967,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2058,6 +2075,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2148,6 +2166,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2243,6 +2262,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2340,6 +2360,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2461,6 +2482,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2637,6 +2659,7 @@ export var whatever = (props) => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -2708,6 +2731,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -2815,6 +2839,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -2898,6 +2923,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -3029,6 +3055,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['left'], view, @@ -3208,6 +3235,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -3349,6 +3377,7 @@ export var whatever = props => ( true, 'var', 'block', + [], defaultPropsParam, ['layout'], rootDiv, @@ -3373,6 +3402,7 @@ export var whatever = props => ( true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], view, @@ -3514,6 +3544,7 @@ export var whatever = (props) => true, 'var', 'expression', + [], defaultPropsParam, ['color'], view, @@ -3584,6 +3615,7 @@ export var whatever = () => true, 'var', 'expression', + [], null, [], view, @@ -3646,6 +3678,7 @@ export var App = (props) => true, 'var', 'expression', + [], defaultPropsParam, [], view, @@ -3714,6 +3747,7 @@ export var App = (props) => true, 'var', 'parenthesized-expression', + [], null, [], view, @@ -3786,6 +3820,7 @@ export var App = (props) => true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], view, @@ -3967,6 +4002,7 @@ var spacing = 20` true, 'var', 'parenthesized-expression', + [], null, [], view, @@ -4105,6 +4141,7 @@ export var whatever = props => { true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], view, @@ -4172,6 +4209,7 @@ export var whatever = props => { true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], view, @@ -4272,6 +4310,7 @@ export var whatever = props => { true, 'var', 'parenthesized-expression', + [], defaultPropsParam, ['color', 'shadowValue', 'there'], view, @@ -4417,6 +4456,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -4494,6 +4534,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -4556,6 +4597,7 @@ export var whatever = props => { false, 'var', 'parenthesized-expression', + [], null, [], jsxElement( @@ -4622,6 +4664,7 @@ export var App = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -4709,6 +4752,7 @@ export var App = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -4763,6 +4807,7 @@ export var App = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -4837,6 +4882,7 @@ export var App = props => { true, 'var', 'parenthesized-expression', + [], defaultPropsParam, [], jsxElement( @@ -5054,6 +5100,7 @@ export var App = props => { true, 'var', 'block', + [], defaultPropsParam, ['layout'], jsxElement( @@ -5252,6 +5299,7 @@ export var App = props => { true, 'var', 'block', + [], defaultPropsParam, [], jsxElement( @@ -5311,6 +5359,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5361,6 +5410,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5469,6 +5519,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5585,6 +5636,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5713,6 +5765,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5879,6 +5932,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, @@ -5916,6 +5970,7 @@ export var whatever = props => { true, 'var', 'block', + [], defaultPropsParam, [], view, 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 cfd9fa9fb1a1..52ef75e7c185 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 @@ -873,6 +873,7 @@ export function utopiaJSXComponentArbitrary(): Arbitrary { isFunction, declarationSyntax, blockOrExpression, + [], defaultPropsParam, [], rootElement, @@ -1477,6 +1478,7 @@ export function clearTopLevelElementSourceMapsUniqueIDsAndEmptyBlocks( return clearArbitraryJSBlockSourceMaps({ ...withoutUID, javascript: blockCode, + sourceMap: null, }) } case 'IMPORT_STATEMENT': diff --git a/editor/src/core/workers/parser-printer/parser-printer.ts b/editor/src/core/workers/parser-printer/parser-printer.ts index 2655271ed5b5..011e1a4613c6 100644 --- a/editor/src/core/workers/parser-printer/parser-printer.ts +++ b/editor/src/core/workers/parser-printer/parser-printer.ts @@ -109,6 +109,7 @@ import { markedAsExported, markedAsDefault, parseParams, + parseAttributeExpression, } from './parser-printer-parsing' import { jsonToExpression } from './json-to-expression' import { difference } from '../../shared/set-utils' @@ -122,6 +123,8 @@ import type { Optic } from '../../../core/shared/optics/optics' import { modify } from '../../../core/shared/optics/optic-utilities' import { identifyValuesDefinedInNode } from './parser-printer-expressions' import { isParseableFile } from '../../shared/file-utils' +import type { FunctionWrap } from 'utopia-shared/src/types/element-template' +import { simpleFunctionWrap } from 'utopia-shared/src/types/element-template' const BakedInStoryboardVariableName = 'storyboard' @@ -722,6 +725,25 @@ function getModifersForComponent( return result } +function printFunctionWrapping( + printOptions: PrintCodeOptions, + imports: Imports, + statement: TS.Expression, + functionWrapping: Array, +): TS.Expression { + return functionWrapping.reduceRight((workingStatement, wrap) => { + switch (wrap.type) { + case 'SIMPLE_FUNCTION_WRAP': + const functionExpression = jsxAttributeToExpression( + wrap.functionExpression, + imports, + printOptions.stripUIDs, + ) + return TS.createCall(functionExpression, undefined, [workingStatement]) + } + }, statement) +} + function printUtopiaJSXComponent( printOptions: PrintCodeOptions, imports: Imports, @@ -800,10 +822,21 @@ function printUtopiaJSXComponent( undefined, bodyForArrowFunction(), ) + const wrappedArrowFunction = printFunctionWrapping( + printOptions, + imports, + arrowFunction, + element.functionWrapping, + ) if (element.name == null) { - elementNode = TS.createExportAssignment(undefined, modifiers, undefined, arrowFunction) + elementNode = TS.createExportAssignment( + undefined, + modifiers, + undefined, + wrappedArrowFunction, + ) } else { - const varDec = TS.createVariableDeclaration(element.name, undefined, arrowFunction) + const varDec = TS.createVariableDeclaration(element.name, undefined, wrappedArrowFunction) const varDecList = TS.createVariableDeclarationList([varDec], nodeFlags) elementNode = TS.createVariableStatement(modifiers, varDecList) } @@ -901,10 +934,6 @@ function optionallyCreateDotDotDotToken(createIt: boolean): TS.DotDotDotToken | return createIt ? TS.createToken(TS.SyntaxKind.DotDotDotToken) : undefined } -function printRawComment(comment: Comment): string { - return `${comment.rawText}${comment.trailingNewLine ? '\n' : ''}` -} - function printArbitraryJSBlock(block: ArbitraryJSBlock): TS.Node { return TS.createUnparsedSourceFile(block.javascript) } @@ -1092,18 +1121,21 @@ interface PossibleCanvasContentsExpression { name: string initializer: TS.Expression declarationSyntax: FunctionDeclarationSyntax + functionWrapping: Array } function possibleCanvasContentsExpression( name: string, initializer: TS.Expression, declarationSyntax: FunctionDeclarationSyntax, + functionWrapping: Array, ): PossibleCanvasContentsExpression { return { type: 'POSSIBLE_CANVAS_CONTENTS_EXPRESSION', name: name, initializer: initializer, declarationSyntax: declarationSyntax, + functionWrapping: functionWrapping, } } @@ -1113,6 +1145,7 @@ interface PossibleCanvasContentsFunction { parameters: TS.NodeArray body: TS.ConciseBody declarationSyntax: FunctionDeclarationSyntax + functionWrapping: Array } function possibleCanvasContentsFunction( @@ -1120,6 +1153,7 @@ function possibleCanvasContentsFunction( parameters: TS.NodeArray, body: TS.ConciseBody, declarationSyntax: FunctionDeclarationSyntax, + functionWrapping: Array, ): PossibleCanvasContentsFunction { return { type: 'POSSIBLE_CANVAS_CONTENTS_FUNCTION', @@ -1127,6 +1161,7 @@ function possibleCanvasContentsFunction( parameters: parameters, body: body, declarationSyntax: declarationSyntax, + functionWrapping: functionWrapping, } } @@ -1165,37 +1200,77 @@ function nodeFlagsForVarLetOrConst(varLetOrConst: VarLetOrConst): TS.NodeFlags { export function looksLikeCanvasElements( sourceFile: TS.SourceFile, node: TS.Node, + sourceText: string, + filename: string, + imports: Imports, + topLevelNames: Array, + propsObjectName: string | null, + existingHighlightBounds: Readonly, + alreadyExistingUIDs: Set, + applySteganography: SteganographyMode, ): Either { if (TS.isVariableStatement(node)) { const variableDeclaration = node.declarationList.declarations[0] if (variableDeclaration != null) { if (variableDeclaration.initializer != null) { const name = variableDeclaration.name.getText(sourceFile) - const initializer = variableDeclaration.initializer const varLetOrConst = getVarLetOrConst(node.declarationList) - if (TS.isArrowFunction(initializer)) { - return right( - possibleCanvasContentsFunction( - name, - initializer.parameters, - initializer.body, - varLetOrConst, - ), - ) - } else if (name === BakedInStoryboardVariableName) { - // FIXME The below case should result in a parsed top level *element*, but instead it is treated - // as a *component*. Unfortunately our use of the storyboard incorrectly relies on it being treated - // as a component. See https://github.com/concrete-utopia/utopia/issues/1718 for more info - return right( - possibleCanvasContentsExpression(name, variableDeclaration.initializer, varLetOrConst), - ) + const initializer = variableDeclaration.initializer + + function processVariable( + currentNode: TS.Node, + functionWrapping: Array, + ): Either { + if (TS.isCallExpression(currentNode)) { + const expressionFromCall = parseAttributeExpression( + sourceFile, + sourceText, + filename, + imports, + topLevelNames, + propsObjectName, + currentNode.expression, + existingHighlightBounds, + alreadyExistingUIDs, + [], + applySteganography, + 'outermost-expression', + ) + if (isRight(expressionFromCall) && currentNode.arguments.length === 1) { + const newFunctionWrapping = [ + ...functionWrapping, + simpleFunctionWrap(expressionFromCall.value.value), + ] + return processVariable(currentNode.arguments[0], newFunctionWrapping) + } + } else if (TS.isArrowFunction(currentNode)) { + return right( + possibleCanvasContentsFunction( + name, + currentNode.parameters, + currentNode.body, + varLetOrConst, + functionWrapping, + ), + ) + } else if (name === BakedInStoryboardVariableName) { + // FIXME The below case should result in a parsed top level *element*, but instead it is treated + // as a *component*. Unfortunately our use of the storyboard incorrectly relies on it being treated + // as a component. See https://github.com/concrete-utopia/utopia/issues/1718 for more info + return right( + possibleCanvasContentsExpression(name, initializer, varLetOrConst, functionWrapping), + ) + } + return left(node) } + + return processVariable(initializer, []) } } } else if (TS.isFunctionDeclaration(node)) { if (node.body != null) { const name = node.name == null ? null : node.name.getText(sourceFile) - return right(possibleCanvasContentsFunction(name, node.parameters, node.body, 'function')) + return right(possibleCanvasContentsFunction(name, node.parameters, node.body, 'function', [])) } } @@ -1537,7 +1612,18 @@ export function parseCode( pushImportStatement(rawImportStatement) } } else { - const possibleDeclaration = looksLikeCanvasElements(sourceFile, topLevelElement) + const possibleDeclaration = looksLikeCanvasElements( + sourceFile, + topLevelElement, + sourceText, + filePath, + imports, + topLevelNames, + null, + highlightBounds, + alreadyExistingUIDs_MUTABLE, + applySteganography, + ) if (isRight(possibleDeclaration)) { const canvasContents = possibleDeclaration.value const { name } = canvasContents @@ -1658,6 +1744,7 @@ export function parseCode( isFunction, canvasContents.declarationSyntax, contents.blockOrExpression, + canvasContents.functionWrapping, foldEither( (_) => null, (param) => param?.value ?? null, @@ -1799,6 +1886,7 @@ export function parseCode( topLevelElement.isFunction, topLevelElement.declarationSyntax, topLevelElement.blockOrExpression, + topLevelElement.functionWrapping, topLevelElement.param, topLevelElement.propsUsed, topLevelElement.rootElement, diff --git a/editor/src/core/workers/ts/ts-worker.spec.ts b/editor/src/core/workers/ts/ts-worker.spec.ts index 6a7007598f83..bef9006c3b66 100644 --- a/editor/src/core/workers/ts/ts-worker.spec.ts +++ b/editor/src/core/workers/ts/ts-worker.spec.ts @@ -153,6 +153,7 @@ const SampleInitTSWorkerMessage: IncomingWorkerMessage = { isFunction: true, declarationSyntax: 'var', blockOrExpression: 'block', + functionWrapping: [], param: { type: 'PARAM', dotDotDotToken: false, diff --git a/utopia-shared/src/types/element-template.ts b/utopia-shared/src/types/element-template.ts index 5adf0419099b..01dd7d49e3ff 100644 --- a/utopia-shared/src/types/element-template.ts +++ b/utopia-shared/src/types/element-template.ts @@ -320,6 +320,24 @@ export type VarLetOrConst = 'var' | 'let' | 'const' export type FunctionDeclarationSyntax = 'function' | VarLetOrConst export type BlockOrExpression = 'block' | 'parenthesized-expression' | 'expression' +export interface SimpleFunctionWrap { + type: 'SIMPLE_FUNCTION_WRAP' + functionExpression: JSExpression +} + +export function simpleFunctionWrap(functionExpression: JSExpression): SimpleFunctionWrap { + return { + type: 'SIMPLE_FUNCTION_WRAP', + functionExpression: functionExpression, + } +} + +export type FunctionWrap = SimpleFunctionWrap + +export function isSimpleFunctionWrap(wrap: FunctionWrap): wrap is SimpleFunctionWrap { + return wrap.type === 'SIMPLE_FUNCTION_WRAP' +} + export interface UtopiaJSXComponent { type: 'UTOPIA_JSX_COMPONENT' name: string | null @@ -331,6 +349,7 @@ export interface UtopiaJSXComponent { isFunction: boolean declarationSyntax: FunctionDeclarationSyntax blockOrExpression: BlockOrExpression + functionWrapping: Array param: Param | null propsUsed: Array rootElement: JSXElementChild