@simpedit-class/local-client v1.0.0
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).
- Run command to start application (e.g.,
jbook serve)- This should start a server on something like
localhost:4005
- This should start a server on something like
- User will write code into an editor
- App bundles code in the browser
- Execute user's code in an
iframewithsandbox="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.
- Code will be provided to Preview as a string. This string must be executed safely.
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
- will need to use a transpiler, like Babel. For this app, we can:
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.
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.tarballwill 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
filterlooks 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.
- User-provided code might throw errors that cause program to crash.
- Solved if execute user's code is contained in an
iframe
- Solved if execute user's code is contained in an
- 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
- e.g., user types in
- User might accdentally run code provided by another malicious user
- Solved if execute user's code in an
iframewith direct communication disabled- Done when setting
sandboxto anything other thanallow-same-origin - Malicious code cannot be used to obtain security information from parent document
- Done when setting
- Solved if execute user's code in an
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
iframeelement does not have asandboxproperty, or has asandbox="allow-same-origin"property - The parent HTML doc and the iframe HTML doc are fetched from the exact same Domain/Port/Protocol (
httpvshttps)
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: 1For the parent document to react into the child document:
document.querySelector("iframe").contentWindow.a;
// output: 3Note: 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, "*");
}Each of these packages will be developed and deployed as separate NPM packages.
The future architecture just describes additional add-ons that can be developed.
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.
2 years ago