diff --git a/js/repl/ASTPanel.js b/js/repl/ASTPanel.js new file mode 100644 index 0000000000..5efe3d43a1 --- /dev/null +++ b/js/repl/ASTPanel.js @@ -0,0 +1,192 @@ +// @flow + +import { css } from "emotion"; +import { colors } from "./styles"; +import React from "react"; +import ReactJson from "react-json-view"; +import { + flatten, + filterFlatten, + unflatten, + deleteFlatten, + mergeFlatten, + reject, +} from "./ASTUtils"; + +type Props = { + className?: string, + src: Object, +}; + +type State = { + src: Object, + flattenEmpty: Object, + flattenSrc: Object, + flattenType: Object, + flattenLocation: Object, + astOption: { + location: boolean, + empty: boolean, + type: boolean, + }, +}; + +const OPTION_ORDER = ["location", "empty", "type"]; + +export default class ASTPanel extends React.Component { + state = { + src: {}, + flattenEmpty: {}, + flattenSrc: {}, + flattenType: {}, + flattenLocation: {}, + astOption: { + autofocus: true, + location: true, + empty: true, + type: true, + }, + }; + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + if (nextProps.src && nextProps.src !== prevState.src) { + let flattenSrc = flatten(nextProps.src); + flattenSrc = reject(flattenSrc, "_"); + const src = unflatten(flattenSrc); + + return { + src: src, + flattenSrc: flattenSrc, + flattenType: filterFlatten(flattenSrc, "type"), + flattenLocation: { + ...filterFlatten(flattenSrc, "start"), + ...filterFlatten(flattenSrc, "end"), + }, + flattenEmpty: filterFlatten(flattenSrc, null, "null"), + }; + } + } + + _onOptionSettingCheck(option: string) { + this.setState(prevState => ({ + astOption: { + ...prevState.astOption, + [option]: !prevState.astOption[option], + }, + })); + this._onChangeJson(option); + } + + _onChangeJson(option: string) { + const { + astOption, + flattenEmpty, + flattenSrc, + flattenType, + flattenLocation, + } = this.state; + + function triggerAstOutput(type) { + const isShow = astOption[type]; + let newSrc = {}; + const types = { + empty: () => { + newSrc = isShow + ? deleteFlatten(flattenSrc, flattenEmpty) + : mergeFlatten(flattenSrc, flattenEmpty); + return newSrc; + }, + type: () => { + newSrc = isShow + ? deleteFlatten(flattenSrc, flattenType) + : mergeFlatten(flattenSrc, flattenType); + return newSrc; + }, + location: () => { + newSrc = isShow + ? deleteFlatten(flattenSrc, flattenLocation) + : mergeFlatten(flattenSrc, flattenLocation); + return newSrc; + }, + default: () => flattenSrc, + }; + return (types[type] || types["default"])(); + } + + const result = triggerAstOutput(option); + this.setState({ flattenSrc: result, src: unflatten(result) }); + } + + render() { + const { src, astOption } = this.state; + const { className = "" } = this.props; + + return ( +
+ {src && ( +
+ {OPTION_ORDER.map(option => ( + + ))} +
+ )} + +
+ ); + } +} + +const styles = { + astWrapper: css({ + height: "100%", + }), + optionWrapper: css({ + display: "flex", + flexDirection: "row", + width: "100%", + justifyContent: "stretch", + color: colors.inverseForegroundLight, + backgroundColor: colors.inverseBackgroundLight, + }), + settingsLabel: css({ + alignItems: "center", + display: "flex", + flexDirection: "colum", + fontSize: "0.875rem", + fontWeight: "normal", + padding: "0.19rem 1rem", + transition: "background-color 250ms ease-in-out, color 250ms ease-in-out", + "&:hover": { + backgroundColor: colors.inverseBackgroundDark, + color: colors.inverseForeground, + }, + }), + inputCheckboxLeft: css({ + margin: "0 0.75rem 0 0 !important", // TODO (bvaughn) Override input[type="checkbox"] style in main.css + "&:disabled": { + opacity: 0.5, + }, + }), + reactJson: { + overflowY: "scroll", + overflow: "show", + width: "100%", + height: "100%", + }, +}; diff --git a/js/repl/ASTUtils.js b/js/repl/ASTUtils.js new file mode 100644 index 0000000000..588fc9d5d2 --- /dev/null +++ b/js/repl/ASTUtils.js @@ -0,0 +1,164 @@ +// https://github.com/hughsk/flat/blob/master/index.js + +function isBuffer(obj) { + return ( + obj != null && + obj.constructor != null && + typeof obj.constructor.isBuffer === "function" && + obj.constructor.isBuffer(obj) + ); +} + +function flatten(target, opts) { + opts = opts || {}; + + const delimiter = opts.delimiter || "."; + const maxDepth = opts.maxDepth; + const output = {}; + + function step(object, prev, currentDepth) { + currentDepth = currentDepth || 1; + Object.keys(object).forEach(function(key) { + const value = object[key]; + const isarray = opts.safe && Array.isArray(value); + const type = Object.prototype.toString.call(value); + const isbuffer = isBuffer(value); + const isobject = type === "[object Object]" || type === "[object Array]"; + const newKey = prev ? prev + delimiter + key : key; + if ( + !isarray && + !isbuffer && + isobject && + Object.keys(value).length && + (!opts.maxDepth || currentDepth < maxDepth) + ) { + return step(value, newKey, currentDepth + 1); + } + output[newKey] = value; + }); + } + step(target); + return output; +} + +function unflatten(target, opts) { + opts = opts || {}; + const delimiter = opts.delimiter || "."; + const overwrite = opts.overwrite || false; + const result = {}; + const isbuffer = isBuffer(target); + if ( + isbuffer || + Object.prototype.toString.call(target) !== "[object Object]" + ) { + return target; + } + + // safely ensure that the key is + // an integer. + function getkey(key) { + const parsedKey = Number(key); + return isNaN(parsedKey) || key.indexOf(".") !== -1 || opts.object + ? key + : parsedKey; + } + + const sortedKeys = Object.keys(target).sort(function(keyA, keyB) { + return keyA.length - keyB.length; + }); + + sortedKeys.forEach(function(key) { + const split = key.split(delimiter); + let key1 = getkey(split.shift()); + let key2 = getkey(split[0]); + let recipient = result; + + while (key2 !== undefined) { + const type = Object.prototype.toString.call(recipient[key1]); + const isobject = type === "[object Object]" || type === "[object Array]"; + + // do not write over falsey, non-undefined values if overwrite is false + if (!overwrite && !isobject && typeof recipient[key1] !== "undefined") { + return; + } + + if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) { + recipient[key1] = typeof key2 === "number" && !opts.object ? [] : {}; + } + + recipient = recipient[key1]; + if (split.length > 0) { + key1 = getkey(split.shift()); + key2 = getkey(split[0]); + } + } + + // unflatten again for 'messy objects' + recipient[key1] = unflatten(target[key], opts); + }); + + return result; +} + +function filterFlatten(flattenSrc, type = "", value) { + const result = Object.keys(flattenSrc) + .filter(key => { + if (type) { + const keys = key.split("."); + return keys.includes(type); + } + if (value) { + const v = value === "null" || value === "undefined" ? null : value; + return flattenSrc[key] === v; + } + }) + .reduce((object, key) => { + object[key] = flattenSrc[key]; + return object; + }, {}); + return result; +} + +function deleteFlatten(currentSrc, deletedSrc) { + const deletedKeys = Object.keys(deletedSrc); + const result = Object.keys(currentSrc).reduce((object, key) => { + !deletedKeys.includes(key) ? (object[key] = currentSrc[key]) : null; + return object; + }, {}); + return result; +} + +function mergeFlatten(currentSrc, nextSrc) { + return { + ...currentSrc, + ...nextSrc, + }; +} + +function reject(obj, char) { + return Object.keys(obj) + .filter(key => !key.includes(char)) + .reduce((o, key) => { + return { + ...o, + [key]: obj[key], + }; + }, {}); +} + +// function reject(obj, keys) { +// const result = Object.keys(obj) +// .filter(k => !keys.includes(k)) +// .map(k => Object.assign({}, { [k]: obj[k] })) +// .reduce((res, o) => Object.assign(res, o), {}); +// return result; +// } + +export { + flatten, + unflatten, + filterFlatten, + deleteFlatten, + mergeFlatten, + reject, +}; diff --git a/js/repl/PluginConfig.js b/js/repl/PluginConfig.js index b259cbfdb2..90dbc65d76 100644 --- a/js/repl/PluginConfig.js +++ b/js/repl/PluginConfig.js @@ -105,6 +105,8 @@ const pluginConfigs: Array = [ ]; const replDefaults: ReplState = { + ast: false, + astContext: {}, babili: false, browsers: "", build: "", diff --git a/js/repl/Repl.js b/js/repl/Repl.js index 3c4a0f8d04..7a2769008b 100644 --- a/js/repl/Repl.js +++ b/js/repl/Repl.js @@ -36,6 +36,7 @@ import { import WorkerApi from "./WorkerApi"; import scopedEval from "./scopedEval"; import { colors, media } from "./styles"; +import ASTPanel from "./ASTPanel"; import type { BabelPresets, @@ -51,6 +52,8 @@ import type { type Props = {}; type State = { + ast: boolean, + astContext: Object, babel: BabelState, code: string, compiled: ?string, @@ -126,6 +129,8 @@ class Repl extends React.Component { // A partial State is defined first b'c this._compile needs it. // The compile helper will then populate the missing State values. this.state = { + ast: persistedState.ast, + astContext: persistedState.astContext, babel: persistedStateToBabelState(persistedState, babelConfig), code: persistedState.code, compiled: null, @@ -203,6 +208,7 @@ class Repl extends React.Component { return (
{ options={options} placeholder="Write code here" /> - + {state.ast ? ( + + ) : ( + + )}
{state.timeTravel && ( { } this._workerApi .compile(code, { + astContext: state.astContext, plugins: state.externalPlugins, debugEnvPreset: state.debugEnvPreset, envConfig: state.envPresetState.isLoaded ? state.envConfig : null, @@ -567,6 +578,8 @@ class Repl extends React.Component { const builtIns = envConfig.isBuiltInsEnabled && envConfig.builtIns; const payload = { + ast: state.ast, + astContext: state.astContext, babili: plugins["babili-standalone"].isEnabled, browsers: envConfig.browsers, build: state.babel.build, @@ -667,11 +680,16 @@ const styles = { codeMirrorPanel: css({ flex: "0 0 50%", }), + astPanel: css({ + flex: "0 0 50%", + overflow: "hidden", + display: "flex", + flexDirection: "column", + }), optionsColumn: css({ flex: "0 0 auto", }), repl: css` - height: 100%; height: calc(100vh - 50px); /* 50px is the header's height */ width: 100%; display: flex; @@ -693,6 +711,7 @@ const styles = { height: "85%", width: "100%", display: "flex", + flexFlow: "wrap", flexDirection: "row", justifyContent: "stretch", overflow: "auto", diff --git a/js/repl/ReplOptions.js b/js/repl/ReplOptions.js index abec4005bd..d5fa4e33b8 100644 --- a/js/repl/ReplOptions.js +++ b/js/repl/ReplOptions.js @@ -46,6 +46,7 @@ type PluginSearch = (value: string) => void; type PluginChange = (plugin: Object) => void; type Props = { + ast: boolean, babelVersion: ?string, className: string, debugEnvPreset: boolean, @@ -180,6 +181,7 @@ class ExpandedContainer extends Component { render() { const { + ast, babelVersion, debugEnvPreset, envConfig, @@ -272,6 +274,15 @@ class ExpandedContainer extends Component { /> Time Travel +