preface

Over time, there are more and more IDE applications online, and they become more and more functional. Such as

  • codesandbox: codesandbox.io
  • Jsbin: jsbin.com/?html outpu… (Personally, this is very light)
  • codepen: codepen.io/pen/
  • jsfiddle: jsfiddle.net/
  • Rookie tool: c.runoob.com/
  • .

As a result, I was curious about the implementation principle of these online ides and wanted to try to implement a similar function myself. Here’s an article detailing how CodesandBox works: portals

We tried to implement a feature where you type code on the left and preview it on the right.

The file structure is as follows

Post the code content separately

(1) package. The json

{
  "name": "react"."version": "1.0.0"."description": "React example starter project"."keywords": [
    "react"."starter"]."main": "src/index.js"."dependencies": {
    "@babel/runtime": "7.13.8"."@babel/standalone": "7.13.11"."acorn": "8.1.0"."escodegen": "2.0.0"."lodash": "4.17.21"."object-path": "0.11.5"."react": "17.0.1"."react-dom": "17.0.1"."react-monaco-editor": "0.43.0"."react-scripts": "4.0.0"
  },
  "devDependencies": {
    "typescript": 4.1.3 ""
  },
  "scripts": {
    "start": "react-scripts start"."build": "react-scripts build"."test": "react-scripts test --env=jsdom"."eject": "react-scripts eject"
  },
  "browserslist": [
    "0.2%" >."not dead"."not ie <= 11"."not op_mini all"]}Copy the code

(2) index. Js

import { StrictMode } from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);
Copy the code

(3) App. JSX

import "./styles.css";
import SandBox from './SandBox';

export default function App() {
  return (
    <div className="App">
      <SandBox />
    </div>
  );
}
Copy the code

(4) the SandBox. The JSX

import React, { useState, useEffect, useRef } from "react";
import _debounce from "lodash/debounce";
import { createEditor } from "./util";
// import MonacoEditor from "react-monaco-editor";

function SandBox() {
  const viewRef = useRef(null);
  const runtimeRef = useRef(null);
  const [code, setCode] = useState(` function HolyCow() { return HolyCow, My God!  } 
       `);

  useEffect(() = >{ runtimeRef.current = createEditor(viewRef.current); runtimeRef.current.run(code); } []);const run = _debounce((newCode) = > {
    runtimeRef.current.run(newCode || code);
  }, 500);

  const onCodeChange = ({ target: { value } }) = > {
    setCode(value);
    run(value);
  };

  return (
    <div className="container" style={{ display: "flex}} ">
      <div className="code-editor" style={{ flex: 1}} >
        <textarea value={code} onChange={onCodeChange} />
      </div>
      <div style={{ flex: 1}} >
        <div className="preview" ref={viewRef} />
      </div>
    </div>
  );
}

export default SandBox;
Copy the code

(5) the util. Js

import React from "react";
import ReactDOM from "react-dom";
import ObjPath from "object-path";
import { parse } from "acorn";
import { generate as generateJs } from "escodegen";
import { transform as babelTransform } from "@babel/standalone";

// Search for the target node
export function findReactNode(ast) {
  // the ast standard structure body
  const { body } = ast;

  // Define an iterator
  return body.find((node) = > {
    // React. CreateElement
    const { type } = node;
    // This ObjPath is similar to lodash's get
    const obj = ObjPath.get(node, "expression.callee.object.name");
    const func = ObjPath.get(node, "expression.callee.property.name");

    return (
      type === "ExpressionStatement" &&
      obj === "React" &&
      func === "createElement"
    );
  });
}

// Create method dynamically
export function createEditor(domElement, moduleResolver = () => null) {
  // The run-time input parameter is used by the method
  function render(node) {
    ReactDOM.render(node, domElement);
  }

  / / same as above
  function require(moduleName) {
    return moduleResolver(moduleName);
  }

  / / core
  function getWrapperFunction(code) {
    try {
      // 1. React&ES6 code
      const esCode = babelTransform(code, { presets: ["es2015"."react"] })
        .code;

      ToAst / / 2. The native code (here playing at acorn, Babel, eslint meet the ESTree Spec standard, portal: https://github.com/estree/estree).
      const ast = parse(esCode, {
        sourceType: "module"
      });

      // 3. Our purpose is to put JSX => js and run React. CreateElement
      // So you have to go to the part where JSX is installed first
      const rnode = findReactNode(ast);

      // 4. If you find a run statement, you must wrap the render method outside of React. CreateElemnet to run it
      if (rnode) {
        // Find the location first, so that you can replace it directly later
        const nodeIndex = ast.body.indexOf(rnode);
        // Generate a string and cut out useless information
        const createElSrc = generateJs(rnode).slice(0, -1);
        // Rebuild the modified AST - statements that can be executed
        const renderCallAst = parse(`render(${createElSrc}) `).body[0];
        ast.body[nodeIndex] = renderCallAst;
      }

      // Eval is not efficient, but it is not safe
      // There are many runtime methods, especially in the Node side VM library - runInThisContext etc
      // The first three input parameters are followed by the function body
      return new Function("React"."render"."require", generateJs(ast));
    } catch ({ message }) {
      / / out
      render(<pre style={{ color: "red}} ">{message}</pre>); }}// The front core can't be exposed
  return {
    // View the compilation result
    compile(code) {
      return getWrapperFunction(code);
    },
    // Run directly
    run(code) {
      this.compile(code)(React, render, require);
    },
    // View the generated string
    getCompiledCode(code) {
      returngetWrapperFunction(code).toString(); }}; }Copy the code

(6) styles. CSS

.App {
  font-family: sans-serif;
  text-align: center;
}

.container {
  width: 100vw;
  height: 100vh;
}

.code-editor {
  width: 50%;
}

.code-editor textarea {
  padding: 1em;
  width: 100%;
  height: 100%;
  border: solid 1px # 000;
  min-height: 30em;
  outline: none;
  font-family: "Monaco";
  font-size: 14px;
  background: # 333;
  color: #74b9ff;
}

Copy the code

Source code portal

We see the AST as a link between the preceding and the following, so far we have just implemented a simple edit preview of a single file, what do we do with multiple modules? The AST still needs to look it up, iterate over imported modules, and assemble all dependent modules for execution.