@simpedit-class/local-client

1.0.0 • Public • Published

Interactive Coding Environment

Note: I will reorganize docs later

React application that is able to locally (on the user's personal machine) create text cells and code cells with a preview window beside each cell through the use of the Monaco editor. Multiple programming languages will be able to be configured into this environment (currently only jsx and css files).

example cells image

Steps of Completed Application

  1. Run command to start application (e.g., jbook serve)
    • This should start a server on something like localhost:4005
  2. User will write code into an editor
  3. App bundles code in the browser
  4. Execute user's code in an iframe with sandbox="allow-scripts"

Downside

Some in-browser features will not be accessible to the user's code (e.g., localStorage.getItem("something") will not work) due to the use of the combination of srcDoc and sandbox in the iframe.

Challenges

  1. Code will be provided to Preview as a string. This string must be executed safely.

  2. This code might have advanced JavaScript in it (e.g., JSX) that the browser cannot execute.

    • will need to use a transpiler, like Babel. For this app, we can:
      • setup a backend server to transpile the sent code
      • use an in-browser transpiler
  3. The code might have import statements for other JavaScript or CSS files. These import statements must be dealt with before executing the code.

    • will need to find all the modules the user has imported from NPM

Transpiling & Bundling Locally

  • Removes an extra request to the API server (which means faster code execution).
  • An API server will not have to be maintained.
  • Less complexity - no moving code back and forth.

This calls for webpack needing to built into the react app with a custom plugin to fetch individual files from NPM.

Problem with bundling locally is that webpack does NOT work in the browser.

Solve webpack problem by using a webpack and babel replacement called esbuild.

Esbuild Summary & Use

Contains:

  • build: S => (g(), $.build(S))
  • serve: f serve(S, K)
  • stop: f stop()
  • transform: f transforms(S, K)

transform will attempt to execute transpiling on the code that is user provided.

build bundles the user provided code. Bundling in the browser requires extra setup.

build relies on a file system. If user writes:

import React from "react";

esbuild will look for a filesystem that the browser will not have. The app will use a plugin to intercept the request from esbuild for react code and send a request to the NPM Registry to get the URL to react.

Running the following in a command line:

npm view react dist.tarball

will return https://registry.npmjs.org/react/-/react-18.2.0.tgz. This provides the react source code.

For this app, inside of the tarball is /package/index.js which has the code:

if (process.env.NODE_ENV === 'production') {
    module.exports = require('./cjs/react.production.min.js');
} else {
    module.exports = require('./cjs/react.development.js');
}

esbuild will interpret the require() statements in order to join the needed files.

To help with getting the above JS code, UNPKG will be used (unpkg.com/react) to fetch the above index.js.

Esbuild Bundling Process

To create a bundle in the browser with esbuild, onResolve and onLoad will need to be used.

Both onResolve and onLoad have an object with filter that is a regular expression. The regex controls when onResolve & onLoad are executed. onResolve handles the different types of files attempting to be loaded. e.g., one onResolve may have a filter for loading a JS file, and another for loading a CSS file.

The namespace is similar to filter in that it specifies a set of files. An example of applying an onLoad on only files with a namespace of a:

build.onResolve({...}, async (args: any) => ({ path: args.path, namespace: 'a' }));

build.onLoad({ filter: /.*/, namespace: 'a' }, async (args: any) => {...});
Description Step
Figure out where the index.js file is stored onResolve step
Attempt to load up the index.js file onLoad step
Parse the index.js file, find any import/require/exports
If there are any import/require/exports, figure out where the requested file is onResolve step
Attempt to load that file up onLoad step

onResolve

onResolve will find where index.js is stored. This function overrides esbuild's natural process of finding out what a file's path is.

Only one onResolve function is needed. It can be defined with multiple if statements to determine paths through one filter:

build.onResolve({ filter: /.*/ }, () => {...});

For this application, several onResolve functions will have more specific filters:

build.onResolve({ filter: /^index\.js$/ }, () => ({ path: "index.js" namespace: "a" }));
build.onResolve({ filter: /^\.{1,2}\// }, (args: any) => ({ path:..., namespace: "a" }));
build.onResolve({ filter: /\.*/ }, (...) => {...});
  • This first filter looks for exactly "index.js"
  • The second handles relative paths (i.e., ./ or ../, for something like ./utils)
  • The last will handle the main file of a module.

Considerations Around Code Execution

  • User-provided code might throw errors that cause program to crash.
    • Solved if execute user's code is contained in an iframe
  • User-provided code might mutate the DOM, causing program to crash
    • e.g., user types in document.body.innerHTML = '';, which will wipe out webpage body
    • Solved if iframe's reference pre-installs html framework when user clicks submit
  • User might accdentally run code provided by another malicious user
    • Solved if execute user's code in an iframe with direct communication disabled
      • Done when setting sandbox to anything other than allow-same-origin
      • Malicious code cannot be used to obtain security information from parent document

iframes can help isolate code. An iframe is an html document within another html doucment. iframss can be configured to allow communication between a parent document and a child document.

Direct access between frames is allowed with BOTH of the following:

  • The iframe element does not have a sandbox property, or has a sandbox="allow-same-origin" property
  • The parent HTML doc and the iframe HTML doc are fetched from the exact same Domain/Port/Protocol (http vs https)

If window.a = 1 is run in the parent document and window.a = 3 is run in the child document, the parent can access the child's a and vice-versa with the following:

For the child document to reach into the parent document:

parent.window.a;
// output: 1

For the parent document to react into the child document:

document.querySelector("iframe").contentWindow.a;
// output: 3

Note: For this app, the iframe will use srcDoc instead of src. srcDoc takes a string that will be generated locally. This way, there will be no different Domain/Port/Protocol because content will not be fetched.

Inside of a React component:

const App = () => {
    // run bundler
    const onClick = async () => {
        const result = await ref.current.build({...});

        setCode(result.outputFiles[0].text);
    }

    const html = `
    <script>{code}</script>
        `;

    return (
        <div>
            <iframe srcDoc={html}></iframe>
        </div>
    );
};

The above snippet will have an error when importing packages that contain a closing script tag. The error is due to the string parsed in srcDoc terminating the contents of the script too soon.

The fix is to refactor the const html var's string to have a message event listener and when the code is bundled, post the message via the iframe's ref:

const html = `
<html>
  <head></head>
  <body>
    <div id="root"></div>
    <script>
    window.addEventListener("message", (event) => {
        eval(event.data);
            })
    </script>
  </body>
</html>
`

// run bundler
const onClick = async () => {
    const result = await ref.current.build({...});

    // iframe is a ref to the iframe tag
    iframe.current.contentWindow.postMessage(result.outputFiles[0].text, "*");
}

App's Architectures

Each of these packages will be developed and deployed as separate NPM packages.

App's Current Architecture
CLI jbook
Local Express API @jbook/local-api
Public Express API @jbook/public-api
React App @jbook/local-client

The future architecture just describes additional add-ons that can be developed.

App's Future Architecture
CLI • Needs to know how to start up the Local API • Needs to know how to publish a notebook to the Public API
Local Express API • Needs to serve up the react app • Needs to be able to save/load cells from a file
Public Express API • Needs to serve up the react app • Needs to be able to save/load cells from a database • Needs to handle authentication/permissions/etc.
React App • Needs to make its production assets available to either the local API or the public API

Lerna

Lerna will make it very easy to consume updates between our modules on local machines as the modules are being developed. So, instead of having to re-publish to npm and re-installing into node_modules, lerna will setup a link from node_modules to a copy of the package on local machines.

Readme

Keywords

none

Package Sidebar

Install

npm i @simpedit-class/local-client

Weekly Downloads

0

Version

1.0.0

License

none

Unpacked Size

12.5 MB

Total Files

1063

Last publish

Collaborators

  • justin0979