From b3ffe8652f97a1d83ea058fb43b0fe83fbd1b73f Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 10 Mar 2021 18:13:38 -0500 Subject: [PATCH 1/3] Revert "Revert "Merge pull request #1756 from processing/bug/interactive-console"" This reverts commit e65c3e5ff9b7b129e0f67f829dee403870a63e32. --- client/components/useAsModal.jsx | 2 +- client/images/console-command-contrast.svg | 13 ++ client/images/console-command-dark.svg | 13 ++ client/images/console-command-light.svg | 13 ++ client/images/console-result-contrast.svg | 22 +++ client/images/console-result-dark.svg | 22 +++ client/images/console-result-light.svg | 22 +++ client/modules/IDE/components/Console.jsx | 100 ++++++++----- .../modules/IDE/components/ConsoleInput.jsx | 139 ++++++++++++++++++ client/modules/IDE/components/Editor.jsx | 1 + .../modules/IDE/components/PreviewFrame.jsx | 69 +-------- .../IDE/hooks}/custom-hooks.js | 0 client/modules/IDE/pages/MobileIDEView.jsx | 2 +- client/styles/abstracts/_variables.scss | 16 +- client/styles/components/_console-input.scss | 52 +++++++ client/styles/components/_console.scss | 28 ++-- client/styles/main.scss | 1 + client/utils/dispatcher.js | 51 +++++++ client/utils/evaluateExpression.js | 29 ++++ client/utils/previewEntry.js | 37 ++++- 20 files changed, 518 insertions(+), 114 deletions(-) create mode 100644 client/images/console-command-contrast.svg create mode 100644 client/images/console-command-dark.svg create mode 100644 client/images/console-command-light.svg create mode 100644 client/images/console-result-contrast.svg create mode 100644 client/images/console-result-dark.svg create mode 100644 client/images/console-result-light.svg create mode 100644 client/modules/IDE/components/ConsoleInput.jsx rename client/{utils => modules/IDE/hooks}/custom-hooks.js (100%) create mode 100644 client/styles/components/_console-input.scss create mode 100644 client/utils/dispatcher.js create mode 100644 client/utils/evaluateExpression.js diff --git a/client/components/useAsModal.jsx b/client/components/useAsModal.jsx index 2fbfd5a96c..1ea8d71aa7 100644 --- a/client/components/useAsModal.jsx +++ b/client/components/useAsModal.jsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { useModalBehavior } from '../utils/custom-hooks'; +import { useModalBehavior } from '../modules/IDE/hooks/custom-hooks'; const BackgroundOverlay = styled.div` position: fixed; diff --git a/client/images/console-command-contrast.svg b/client/images/console-command-contrast.svg new file mode 100644 index 0000000000..a08ee8b0f7 --- /dev/null +++ b/client/images/console-command-contrast.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-command-dark.svg b/client/images/console-command-dark.svg new file mode 100644 index 0000000000..a08ee8b0f7 --- /dev/null +++ b/client/images/console-command-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-command-light.svg b/client/images/console-command-light.svg new file mode 100644 index 0000000000..3def60e1fb --- /dev/null +++ b/client/images/console-command-light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-contrast.svg b/client/images/console-result-contrast.svg new file mode 100644 index 0000000000..36fa2e36d4 --- /dev/null +++ b/client/images/console-result-contrast.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-dark.svg b/client/images/console-result-dark.svg new file mode 100644 index 0000000000..7c5c5c9b27 --- /dev/null +++ b/client/images/console-result-dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-light.svg b/client/images/console-result-light.svg new file mode 100644 index 0000000000..a51506e00a --- /dev/null +++ b/client/images/console-result-light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index 14e4e8d922..829c0dd656 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; @@ -25,33 +25,51 @@ import debugContrastUrl from '../../../images/console-debug-contrast.svg?byUrl'; import infoLightUrl from '../../../images/console-info-light.svg?byUrl'; import infoDarkUrl from '../../../images/console-info-dark.svg?byUrl'; import infoContrastUrl from '../../../images/console-info-contrast.svg?byUrl'; +import ConsoleInput from './ConsoleInput'; + +import commandLightUrl from '../../../images/console-command-light.svg?byUrl'; +import resultLightUrl from '../../../images/console-result-light.svg?byUrl'; +import commandDarkUrl from '../../../images/console-command-dark.svg?byUrl'; +import resultDarkUrl from '../../../images/console-result-dark.svg?byUrl'; +import commandContrastUrl from '../../../images/console-command-contrast.svg?byUrl'; +import resultContrastUrl from '../../../images/console-result-contrast.svg?byUrl'; import UpArrowIcon from '../../../images/up-arrow.svg'; import DownArrowIcon from '../../../images/down-arrow.svg'; import * as IDEActions from '../../IDE/actions/ide'; import * as ConsoleActions from '../../IDE/actions/console'; -import { useDidUpdate } from '../../../utils/custom-hooks'; +import { useDidUpdate } from '../hooks/custom-hooks'; +import useHandleMessageEvent from '../hooks/useHandleMessageEvent'; +import { listen } from '../../../utils/dispatcher'; const getConsoleFeedStyle = (theme, times, fontSize) => { - const style = {}; + const style = { + BASE_FONT_FAMILY: 'Inconsolata, monospace' + }; const CONSOLE_FEED_LIGHT_ICONS = { LOG_WARN_ICON: `url(${warnLightUrl})`, LOG_ERROR_ICON: `url(${errorLightUrl})`, LOG_DEBUG_ICON: `url(${debugLightUrl})`, - LOG_INFO_ICON: `url(${infoLightUrl})` + LOG_INFO_ICON: `url(${infoLightUrl})`, + LOG_COMMAND_ICON: `url(${commandLightUrl})`, + LOG_RESULT_ICON: `url(${resultLightUrl})` }; const CONSOLE_FEED_DARK_ICONS = { LOG_WARN_ICON: `url(${warnDarkUrl})`, LOG_ERROR_ICON: `url(${errorDarkUrl})`, LOG_DEBUG_ICON: `url(${debugDarkUrl})`, - LOG_INFO_ICON: `url(${infoDarkUrl})` + LOG_INFO_ICON: `url(${infoDarkUrl})`, + LOG_COMMAND_ICON: `url(${commandDarkUrl})`, + LOG_RESULT_ICON: `url(${resultDarkUrl})` }; const CONSOLE_FEED_CONTRAST_ICONS = { LOG_WARN_ICON: `url(${warnContrastUrl})`, LOG_ERROR_ICON: `url(${errorContrastUrl})`, LOG_DEBUG_ICON: `url(${debugContrastUrl})`, - LOG_INFO_ICON: `url(${infoContrastUrl})` + LOG_INFO_ICON: `url(${infoContrastUrl})`, + LOG_COMMAND_ICON: `url(${commandContrastUrl})`, + LOG_RESULT_ICON: `url(${resultContrastUrl})` }; const CONSOLE_FEED_SIZES = { TREENODE_LINE_HEIGHT: 1.2, @@ -94,6 +112,7 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { const Console = ({ t }) => { const consoleEvents = useSelector((state) => state.console); const isExpanded = useSelector((state) => state.ide.consoleIsExpanded); + const isPlaying = useSelector((state) => state.ide.isPlaying); const { theme, fontSize } = useSelector((state) => state.preferences); const { @@ -103,17 +122,20 @@ const Console = ({ t }) => { dispatchConsoleEvent } = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch()); - useDidUpdate(() => { - clearConsole(); - dispatchConsoleEvent(consoleEvents); - }, [theme, fontSize]); - const cm = useRef({}); useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); + const handleMessageEvent = useHandleMessageEvent(); + useEffect(() => { + const unsubscribe = listen(handleMessageEvent); + return function cleanup() { + unsubscribe(); + }; + }); + const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded @@ -147,29 +169,39 @@ const Console = ({ t }) => { -
- {consoleEvents.map((consoleEvent) => { - const { method, times } = consoleEvent; - return ( -
- {times > 1 && ( -
- {times} -
- )} - -
- ); - })} +
+
+ {consoleEvents.map((consoleEvent) => { + const { method, times } = consoleEvent; + return ( +
+ {times > 1 && ( +
+ {times} +
+ )} + +
+ ); + })} +
+ {isExpanded && isPlaying && ( + + )}
); diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx new file mode 100644 index 0000000000..d70fcec087 --- /dev/null +++ b/client/modules/IDE/components/ConsoleInput.jsx @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CodeMirror from 'codemirror'; +import { Encode } from 'console-feed'; + +import RightArrowIcon from '../../../images/right-arrow.svg'; +import { dispatch } from '../../../utils/dispatcher'; + +// heavily inspired by +// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx + +class ConsoleInput extends React.Component { + constructor(props) { + super(props); + this.state = { + commandHistory: [], + commandCursor: -1 + }; + } + + componentDidMount() { + this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line + theme: `p5-${this.props.theme}`, + scrollbarStyle: null, + keymap: 'sublime', + mode: 'javascript', + inputStyle: 'contenteditable' + }); + + this._cm.on('keydown', (cm, e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + const value = cm.getValue(); + if (value.trim(' ') === '') { + return false; + } + const messages = [ + { log: Encode({ method: 'command', data: [value] }) } + ]; + const consoleEvent = [{ method: 'command', data: [value] }]; + dispatch({ + source: 'console', + messages + }); + this.props.dispatchConsoleEvent(consoleEvent); + cm.setValue(''); + this.setState((state) => ({ + commandCursor: -1, + commandHistory: [value, ...state.commandHistory] + })); + } else if (e.key === 'ArrowUp') { + const lineNumber = this._cm.getDoc().getCursor().line; + if (lineNumber !== 0) { + return false; + } + + this.setState((state) => { + const newCursor = Math.min( + state.commandCursor + 1, + state.commandHistory.length - 1 + ); + this._cm.getDoc().setValue(state.commandHistory[newCursor] || ''); + const cursorPos = this._cm.getDoc().getLine(0).length - 1; + this._cm.getDoc().setCursor({ line: 0, ch: cursorPos }); + return { commandCursor: newCursor }; + }); + } else if (e.key === 'ArrowDown') { + const lineNumber = this._cm.getDoc().getCursor().line; + const lineCount = this._cm.getValue().split('\n').length; + if (lineNumber + 1 !== lineCount) { + return false; + } + + this.setState((state) => { + const newCursor = Math.max(state.commandCursor - 1, -1); + this._cm.getDoc().setValue(state.commandHistory[newCursor] || ''); + const newLineCount = this._cm.getValue().split('\n').length; + const newLine = this._cm.getDoc().getLine(newLineCount); + const cursorPos = newLine ? newLine.length - 1 : 1; + this._cm.getDoc().setCursor({ line: lineCount, ch: cursorPos }); + return { commandCursor: newCursor }; + }); + } + return true; + }); + + this._cm.getWrapperElement().style[ + 'font-size' + ] = `${this.props.fontSize}px`; + } + + componentDidUpdate(prevProps) { + this._cm.setOption('theme', `p5-${this.props.theme}`); + this._cm.getWrapperElement().style[ + 'font-size' + ] = `${this.props.fontSize}px`; + this._cm.refresh(); + } + + componentWillUnmount() { + this._cm = null; + } + + render() { + return ( +
+
+
+
{ + this.codemirrorContainer = element; + }} + className="console__editor" + /> +
+ ); + } +} + +ConsoleInput.propTypes = { + theme: PropTypes.string.isRequired, + dispatchConsoleEvent: PropTypes.func.isRequired, + fontSize: PropTypes.number.isRequired +}; + +export default ConsoleInput; diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 6ba1a6c5d0..5be24e3ff9 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -222,6 +222,7 @@ class Editor extends React.Component { const oldDoc = this._cm.swapDoc(this._docs[this.props.file.id]); this._docs[prevProps.file.id] = oldDoc; this._cm.focus(); + if (!prevProps.unsavedChanges) { setTimeout(() => this.props.setUnsavedChanges(false), 400); } diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index 2762cd0bac..a522726068 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -1,14 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; -import { isEqual } from 'lodash'; +// import escapeStringRegexp from 'escape-string-regexp'; import srcDoc from 'srcdoc-polyfill'; import loopProtect from 'loop-protect'; import { JSHINT } from 'jshint'; import decomment from 'decomment'; import classNames from 'classnames'; import { connect } from 'react-redux'; -import { Decode } from 'console-feed'; import { getBlobUrl } from '../actions/files'; import { resolvePathToFile } from '../../../../server/utils/filePath'; import { @@ -24,6 +23,7 @@ import { startTag, getAllScriptOffsets } from '../../../utils/consoleUtils'; +import { registerFrame } from '../../../utils/dispatcher'; import { getHTMLFile } from '../reducers/files'; @@ -57,18 +57,18 @@ const shouldRenderSketch = (props, prevProps = undefined) => { class PreviewFrame extends React.Component { constructor(props) { super(props); - this.handleConsoleEvent = this.handleConsoleEvent.bind(this); + + this.iframe = React.createRef(); } componentDidMount() { - window.addEventListener('message', this.handleConsoleEvent); - const props = { ...this.props, previewIsRefreshing: this.props.previewIsRefreshing, isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying }; if (shouldRenderSketch(props)) this.renderSketch(); + registerFrame(this.iframe.current.contentWindow); } componentDidUpdate(prevProps) { @@ -78,60 +78,12 @@ class PreviewFrame extends React.Component { } componentWillUnmount() { - window.removeEventListener('message', this.handleConsoleEvent); - const iframeBody = this.iframeElement.contentDocument.body; + const iframeBody = this.iframe.current.contentDocument.body; if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); } } - handleConsoleEvent(messageEvent) { - if (Array.isArray(messageEvent.data)) { - const decodedMessages = messageEvent.data.map((message) => - Object.assign(Decode(message.log), { - source: message.source - }) - ); - - decodedMessages.every((message, index, arr) => { - const { data: args } = message; - let hasInfiniteLoop = false; - Object.keys(args).forEach((key) => { - if ( - typeof args[key] === 'string' && - args[key].includes('Exiting potential infinite loop') - ) { - this.props.stopSketch(); - this.props.expandConsole(); - hasInfiniteLoop = true; - } - }); - if (hasInfiniteLoop) { - return false; - } - if (index === arr.length - 1) { - Object.assign(message, { times: 1 }); - return false; - } - const cur = Object.assign(message, { times: 1 }); - const nextIndex = index + 1; - while ( - isEqual(cur.data, arr[nextIndex].data) && - cur.method === arr[nextIndex].method - ) { - cur.times += 1; - arr.splice(nextIndex, 1); - if (nextIndex === arr.length) { - return false; - } - } - return true; - }); - - this.props.dispatchConsoleEvent(decodedMessages); - } - } - addLoopProtect(sketchDoc) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); @@ -382,7 +334,7 @@ class PreviewFrame extends React.Component { } renderSketch() { - const doc = this.iframeElement; + const doc = this.iframe.current; const localFiles = this.injectLocalFiles(); if (this.props.isPlaying) { this.props.clearConsole(); @@ -411,9 +363,7 @@ class PreviewFrame extends React.Component { role="main" frameBorder="0" title="sketch preview" - ref={(element) => { - this.iframeElement = element; - }} + ref={this.iframe} sandbox={sandboxAttributes} /> ); @@ -437,13 +387,10 @@ PreviewFrame.propTypes = { id: PropTypes.string.isRequired }) ).isRequired, - dispatchConsoleEvent: PropTypes.func.isRequired, endSketchRefresh: PropTypes.func.isRequired, previewIsRefreshing: PropTypes.bool.isRequired, fullView: PropTypes.bool, setBlobUrl: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, - expandConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired, cmController: PropTypes.shape({ getContent: PropTypes.func diff --git a/client/utils/custom-hooks.js b/client/modules/IDE/hooks/custom-hooks.js similarity index 100% rename from client/utils/custom-hooks.js rename to client/modules/IDE/hooks/custom-hooks.js diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 128db1a12a..201ad4e5c0 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -46,7 +46,7 @@ import { getIsUserOwner } from '../selectors/users'; import { useEffectWithComparison, useEventListener -} from '../../../utils/custom-hooks'; +} from '../hooks/custom-hooks'; import * as device from '../../../utils/device'; diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index 8176a275a5..b9a16d87a3 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -65,7 +65,11 @@ $themes: ( icon-toast-hover-color: $lightest, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $light, - console-color: $lightest, + console-input-background-color: $lightest, + console-color: $darker, + console-logged-times-color: $lightest, + console-arrow-color: $middle-gray, + console-active-arrow-color: #0071AD, console-header-background-color: $medium-light, console-header-color: $darker, console-info-background-color: #5276B7, @@ -143,7 +147,11 @@ $themes: ( icon-toast-hover-color: $lightest, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $dark, + console-input-background-color: $darker, console-color: $lightest, + console-logged-times-color: $dark, + console-arrow-color: $medium-light, + console-active-arrow-color: #097BB3, console-header-background-color: $medium-dark, console-header-color: $lightest, console-info-background-color: #5276B7, @@ -219,7 +227,11 @@ $themes: ( icon-toast-hover-color: $yellow, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $dark, - console-color: $black, + console-input-background-color: $darker, + console-color: $lightest, + console-logged-times-color: $darker, + console-arrow-color: $lightest, + console-active-arrow-color: $dodgerblue, console-header-background-color: $medium-dark, console-header-color: $lightest, console-info-background-color: $lightsteelblue, diff --git a/client/styles/components/_console-input.scss b/client/styles/components/_console-input.scss new file mode 100644 index 0000000000..84db1f3387 --- /dev/null +++ b/client/styles/components/_console-input.scss @@ -0,0 +1,52 @@ +.console__input { + width: 100%; + display: flex; + align-items: start; + @include themify() { + background-color: getThemifyVariable('console-input-background-color'); + } +} + +.console__input .console-active__arrow { + width: auto; + height: 38%; + & path { + @include themify() { + fill: getThemifyVariable('console-active-arrow-color'); + opacity: 1; + } + } +} + +.console-active__arrow-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + margin-left: #{10 / $base-font-size}rem; +} + +.console__editor { + margin-left: #{15 / $base-font-size}rem; + flex: 1; + & .CodeMirror { + height: auto; + } + & .CodeMirror-lines { + padding-top: #{2 / $base-font-size}rem; + } +} + +.console__editor .CodeMirror { + border: none; + font-family: Inconsolata,monospace; + @include themify() { + background-color: getThemifyVariable('console-input-background-color'); + } + + .CodeMirror-line { + @include themify() { + color: getThemifyVariable('console-color'); + } + } +} \ No newline at end of file diff --git a/client/styles/components/_console.scss b/client/styles/components/_console.scss index 29e26f62c5..3874102c16 100644 --- a/client/styles/components/_console.scss +++ b/client/styles/components/_console.scss @@ -3,12 +3,11 @@ background: getThemifyVariable('console-background-color'); border-color: getThemifyVariable('ide-border-color'); } - border-left: 1px solid; - border-right: 1px solid; + border-left: #{1 / $base-font-size}rem solid; + border-right: #{1 / $base-font-size}rem solid; width: 100%; height: 100%; z-index: 1000; - overflow: hidden; display: flex; flex-direction: column; @@ -16,14 +15,6 @@ position:relative; text-align:left; } - - .preview-console__message { - @include themify() { - color: getThemifyVariable('console-color'); - } - flex: 1 0 auto; - position: relative; - } } .preview-console__header { @@ -49,6 +40,7 @@ .preview-console__messages { display: flex; + flex: 1; flex-direction: column; overflow-y: auto; } @@ -109,6 +101,7 @@ font-weight: bold; margin: #{2 / $base-font-size}rem 0 0 #{8 / $base-font-size}rem; + border-radius: #{10 / $base-font-size}rem; padding: #{1 / $base-font-size}rem #{4 / $base-font-size}rem; z-index: 100; left: 0; @@ -135,3 +128,16 @@ } } } + +.preview-console__body { + display: flex; + flex-direction: column; + height: calc(100% - #{30 / $base-font-size}rem); + + .preview-console__message { + position: relative; + @include themify() { + color: getThemifyVariable('console-logged-times-color'); + } + } +} diff --git a/client/styles/main.scss b/client/styles/main.scss index 8ba66af71e..b9e4fa53c4 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -44,6 +44,7 @@ @import 'components/keyboard-shortcuts'; @import 'components/copyable-input'; @import 'components/feedback'; +@import 'components/console-input'; @import 'components/loader'; @import 'components/uploader'; @import 'components/tabs'; diff --git a/client/utils/dispatcher.js b/client/utils/dispatcher.js new file mode 100644 index 0000000000..a05631b054 --- /dev/null +++ b/client/utils/dispatcher.js @@ -0,0 +1,51 @@ +// Inspired by +// https://github.com/codesandbox/codesandbox-client/blob/master/packages/codesandbox-api/src/dispatcher/index.ts + +let frame = null; +let listener = null; +const { origin } = window; + +export function registerFrame(newFrame) { + frame = newFrame; +} + +function notifyListener(message) { + if (listener) listener(message); +} + +function notifyFrame(message) { + const rawMessage = JSON.parse(JSON.stringify(message)); + if (frame && frame.postMessage) { + frame.postMessage(rawMessage, origin); + } +} + +export function dispatch(message) { + if (!message) return; + + notifyListener(message); + notifyFrame(message); +} + +/** + * Call callback to remove listener + */ +export function listen(callback) { + listener = callback; + return () => { + listener = null; + }; +} + +function eventListener(e) { + const { data } = e; + + // Removing the origin check for now + // I wonder if this is what is breaking production + // if (data && e.origin === origin) { + if (data) { + notifyListener(data); + } +} + +window.addEventListener('message', eventListener); diff --git a/client/utils/evaluateExpression.js b/client/utils/evaluateExpression.js new file mode 100644 index 0000000000..6e277d5d0f --- /dev/null +++ b/client/utils/evaluateExpression.js @@ -0,0 +1,29 @@ +function __makeEvaluateExpression(evalInClosure) { + return (expr) => + evalInClosure(` + ${expr}`); +} + +function evaluateExpression() { + return __makeEvaluateExpression((expr) => { + let newExpr = expr; + let result = null; + let error = false; + try { + try { + const wrapped = `(${expr})`; + const validate = new Function(wrapped); // eslint-disable-line + newExpr = wrapped; // eslint-disable-line + } catch (e) { + // We shouldn't wrap the expression + } + result = (0, eval)(newExpr); // eslint-disable-line + } catch (e) { + result = `${e.name}: ${e.message}`; + error = true; + } + return { result, error }; + }); +} + +export default evaluateExpression(); diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 59858938b5..4d560d9e20 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -1,5 +1,6 @@ import loopProtect from 'loop-protect'; -import { Hook } from 'console-feed'; +import { Hook, Decode, Encode } from 'console-feed'; +import evaluateExpression from './evaluateExpression'; window.loopProtect = loopProtect; @@ -7,13 +8,41 @@ const consoleBuffer = []; const LOGWAIT = 500; Hook(window.console, (log) => { consoleBuffer.push({ - log, - source: 'sketch' + log }); }); setInterval(() => { if (consoleBuffer.length > 0) { - window.parent.postMessage(consoleBuffer, '*'); + const message = { + messages: consoleBuffer, + source: 'sketch' + }; + window.parent.postMessage(message, window.origin); consoleBuffer.length = 0; } }, LOGWAIT); + +function handleMessageEvent(e) { + if (window.origin !== e.origin) return; + const { data } = e; + const { source, messages } = data; + if (source === 'console' && Array.isArray(messages)) { + const decodedMessages = messages.map((message) => Decode(message.log)); + decodedMessages.forEach((message) => { + const { data: args } = message; + const { result, error } = evaluateExpression(args); + const resultMessages = [ + { log: Encode({ method: error ? 'error' : 'result', data: [result] }) } + ]; + window.parent.postMessage( + { + messages: resultMessages, + source: 'sketch' + }, + window.origin + ); + }); + } +} + +window.addEventListener('message', handleMessageEvent); From 0b9581d73121c9a6576e0ddcdac987e0f02dfade Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 10 Mar 2021 18:18:06 -0500 Subject: [PATCH 2/3] Revert "[#1756] Maybe fixes console in production" This reverts commit ab295522023f2fc0eb8dfca7f42a226f0849782c. --- client/utils/dispatcher.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/utils/dispatcher.js b/client/utils/dispatcher.js index a05631b054..6745840999 100644 --- a/client/utils/dispatcher.js +++ b/client/utils/dispatcher.js @@ -40,10 +40,7 @@ export function listen(callback) { function eventListener(e) { const { data } = e; - // Removing the origin check for now - // I wonder if this is what is breaking production - // if (data && e.origin === origin) { - if (data) { + if (data && e.origin === origin) { notifyListener(data); } } From 7594f403b1509d3bff556877098720e901a4cfdb Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Thu, 11 Mar 2021 17:57:00 -0500 Subject: [PATCH 3/3] [#667][##1756] Add cache-busting to previewScripts.js --- client/modules/IDE/components/PreviewFrame.jsx | 3 ++- server/views/index.js | 2 +- webpack/config.prod.js | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index a522726068..ea5cee18b0 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -35,6 +35,7 @@ import { } from '../actions/preferences'; import { setBlobUrl } from '../actions/files'; import { clearConsole, dispatchConsoleEvent } from '../actions/console'; +import getConfig from '../../../utils/getConfig'; const shouldRenderSketch = (props, prevProps = undefined) => { const { isPlaying, previewIsRefreshing, fullView } = props; @@ -168,7 +169,7 @@ class PreviewFrame extends React.Component { } const previewScripts = sketchDoc.createElement('script'); - previewScripts.src = '/previewScripts.js'; + previewScripts.src = getConfig('PREVIEW_SCRIPTS_URL'); sketchDoc.head.appendChild(previewScripts); const sketchDocString = `\n${sketchDoc.documentElement.outerHTML}`; diff --git a/server/views/index.js b/server/views/index.js index ecc269c064..e60199e51d 100644 --- a/server/views/index.js +++ b/server/views/index.js @@ -34,7 +34,7 @@ export function renderIndex() { window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined}; window.process.env.MOBILE_ENABLED = ${process.env.MOBILE_ENABLED ? `${process.env.MOBILE_ENABLED}` : undefined}; window.process.env.TRANSLATIONS_ENABLED = ${process.env.TRANSLATIONS_ENABLED === 'true' ? true : false}; - + window.process.env.PREVIEW_SCRIPTS_URL = '${process.env.NODE_ENV === 'production' ? `${assetsManifest['/previewScripts.js']}` : '/previewScripts.js'}'; diff --git a/webpack/config.prod.js b/webpack/config.prod.js index 5e804b40b6..4939d291a2 100644 --- a/webpack/config.prod.js +++ b/webpack/config.prod.js @@ -11,6 +11,8 @@ if (process.env.NODE_ENV === "development") { require('dotenv').config(); } +const sharedObj = {}; + module.exports = [{ devtool: 'source-map', mode: 'production', @@ -156,6 +158,8 @@ module.exports = [{ plugins: [ new WebpackManifestPlugin({ basePath: '/', + filename: 'manifest.json', + seed: sharedObj }), new MiniCssExtractPlugin({ filename: 'app.[hash].css', @@ -170,7 +174,7 @@ module.exports = [{ }, { entry: { - app: [ + previewScripts: [ path.resolve(__dirname, '../client/utils/previewEntry.js') ] }, @@ -179,7 +183,7 @@ module.exports = [{ mode: 'production', output: { path: path.resolve(__dirname, '../dist/static'), - filename: 'previewScripts.js', + filename: 'previewScripts.[hash].js', publicPath: '/' }, resolve: { @@ -208,4 +212,11 @@ module.exports = [{ parallel: true })], }, + plugins: [ + new WebpackManifestPlugin({ + basePath: '/', + filename: 'manifest.json', + seed: sharedObj + }) + ] }];