Skip to content

Commit d686eb5

Browse files
committed
Make a wicked react editor
1 parent 9631d5f commit d686eb5

File tree

2 files changed

+205
-26
lines changed

2 files changed

+205
-26
lines changed

apps/components_guide_web/lib/components_guide_web/templates/react_typescript/editor-prolog.html.eex

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- Use https://github.com/tau-prolog/tau-prolog instead? -->
2+
13
<script src="https://unpkg.com/monaco-editor@latest/min/vs/loader.js"></script>
24

35
<script type="module">
@@ -205,14 +207,9 @@ require(["vs/editor/editor.main"], function () {
205207
model: monaco.editor.createModel(value, 'prolog'),
206208
value,
207209
theme,
208-
minimap: false
209-
});
210-
const output = monaco.editor.create(document.getElementById('output'), {
211-
language: 'javascript',
212-
value: '//',
213-
theme,
214-
readOnly: true,
215-
minimap: false
210+
minimap: {
211+
enabled: false
212+
}
216213
});
217214
const onEdit = () => {
218215
const body = input.getValue();
@@ -248,7 +245,8 @@ inputForm.addEventListener('submit', {
248245
const value = data.get('query');
249246
console.log(value, new URLSearchParams(data).toString());
250247
query2(bindings, 'ancestor', 'java', 'erlang');
251-
// form.reset();
248+
249+
form.elements.query.value = "";
252250
}
253251
});
254252
</script>
@@ -262,5 +260,4 @@ inputForm.addEventListener('submit', {
262260
</form>
263261
<div class="flex-container" id="container" style="display: flex; min-height: 100vh;">
264262
<div id="input" style="flex: 1;"></div>
265-
<div id="output" style="flex: 1;"></div>
266263
</div>

apps/components_guide_web/lib/components_guide_web/templates/react_typescript/editor.html.eex

Lines changed: 198 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- See https://www.sanity.io/guides/server-side-rendering-deno-react -->
2+
13
<script src="https://unpkg.com/monaco-editor@latest/min/vs/loader.js"></script>
24

35
<script type="module">
@@ -25,14 +27,104 @@ const theme = window.matchMedia &&
2527
window.matchMedia('(prefers-color-scheme: dark)').matches
2628
? 'vs-dark' : undefined;
2729

28-
const value = `
29-
const a = 1 + 1;
30+
let value = `
31+
import { flavors } from "https://gist.githubusercontent.com/BurntCaramel/d9d2ca7ed6f056632696709a2ae3c413/raw/0234322cf854d52e2f2bd33aa37e8c8b00f9df0a/1.js";
32+
33+
const a = 1 + 1 + flavors.length;
3034

3135
export function Example() {
3236
return a + 4;
3337
}
3438
`.trim();
3539

40+
const prefix = `
41+
//import React from "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.profiling.min.js/+esm";
42+
//import ReactDOM from "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.profiling.min.js/+esm";
43+
//import ReactDOMServer from "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom-server.profiling.min.js/+esm";
44+
import React, { useReducer, useCallback, useEffect, useState, useMemo } from "https://jspm.dev/[email protected]";
45+
import ReactDOM from "https://jspm.dev/[email protected]/profiling";
46+
import ReactDOMServer from "https://jspm.dev/[email protected]/server";
47+
`;
48+
49+
const suffix = `
50+
class ErrorBoundary extends React.Component {
51+
constructor(props) {
52+
super(props);
53+
54+
this.state = { error: null };
55+
}
56+
57+
static getDerivedStateFromError(error) {
58+
return { error };
59+
}
60+
61+
render() {
62+
if (this.state.error) {
63+
return <div class="flex h-full justify-center items-center text-white bg-red-700"><div>Error: {this.state.error.message}</div></div>;
64+
}
65+
66+
return <>{this.props.children}</>;
67+
}
68+
}
69+
70+
export function Example() {
71+
const wrapped = <React.Profiler id="Navigation" onRender={console.log}><ErrorBoundary><App /></ErrorBoundary></React.Profiler>;
72+
73+
const clientAppEl = document.getElementById('clientApp');
74+
clientAppEl.dispatchEvent(new CustomEvent('reset'));
75+
ReactDOM.render(wrapped, clientAppEl);
76+
clientAppEl.addEventListener('reset', () => {
77+
ReactDOM.unmountComponentAtNode(clientAppEl);
78+
}, { once: true });
79+
80+
try {
81+
return ReactDOMServer.renderToString(wrapped);
82+
} catch (error) {
83+
return \`<!-- Uncaught error: \${error.message} -->\n<div class="flex h-full justify-center items-center text-white bg-red-700"><div>Error: \${error.message}</div></div>\`;
84+
}
85+
}
86+
`;
87+
88+
value = `
89+
import { flavors } from "https://gist.githubusercontent.com/BurntCaramel/d9d2ca7ed6f056632696709a2ae3c413/raw/0234322cf854d52e2f2bd33aa37e8c8b00f9df0a/1.js";
90+
91+
const a = 1 + 1 + flavors.length;
92+
93+
function useTick() {
94+
return useReducer(n => n + 1, 0);
95+
}
96+
97+
function useDebouncer(duration) {
98+
const [count, tick] = useTick();
99+
100+
const effect = useMemo(() => {
101+
let timeout = null;
102+
function clear() {
103+
if (timeout) {
104+
clearTimeout(timeout);
105+
timeout = null;
106+
}
107+
}
108+
return () => {
109+
clear()
110+
timeout = setTimeout(tick, duration);
111+
return clear;
112+
};
113+
}, [duration, tick]);
114+
115+
return [count, effect];
116+
}
117+
118+
export default function App() {
119+
const [count, tick] = useDebouncer(1000);
120+
return <>
121+
<div>Hello!! {flavors.join(" ")}</div>
122+
<button onClick={tick}>Click</button>
123+
<div>{count}</div>
124+
</>;
125+
}
126+
`.trim();
127+
36128
const types = fetch("https://workers.cloudflare.com/index.d.ts", { cache: 'force-cache' })
37129
.then((response) => response.text())
38130
.catch((err) => `// ${err.message}`);
@@ -56,14 +148,18 @@ require(["vs/editor/editor.main"], function () {
56148
model: monaco.editor.createModel(value, 'typescript', 'ts:worker.ts'),
57149
value,
58150
theme,
59-
minimap: false
151+
minimap: {
152+
enabled: false
153+
}
60154
});
61-
const output = monaco.editor.create(document.getElementById('output'), {
62-
language: 'javascript',
63-
value: '//',
155+
const htmlOutput = monaco.editor.create(document.getElementById('htmlOutput'), {
156+
language: 'html',
157+
value: '',
64158
theme,
65159
readOnly: true,
66-
minimap: false
160+
minimap: {
161+
enabled: false
162+
}
67163
});
68164
const statusEl = document.getElementById('status');
69165
const resultEl = document.getElementById('result');
@@ -78,15 +174,94 @@ require(["vs/editor/editor.main"], function () {
78174
});
79175

80176
esbuildPromise
81-
.then(esbuild => esbuild.transform(body, { loader: 'jsx', format: 'iife', globalName: 'exports', }))
82-
.then(content => {
83-
output.getModel().setValue(content.code);
177+
.then(esbuild => {
178+
const httpPlugin = {
179+
name: 'http',
180+
setup(build) {
181+
// Intercept import paths starting with "http:" and "https:" so
182+
// esbuild doesn't attempt to map them to a file system location.
183+
// Tag them with the "http-url" namespace to associate them with
184+
// this plugin.
185+
build.onResolve({ filter: /^https?:\/\// }, args => ({
186+
path: args.path,
187+
namespace: 'http-url',
188+
}))
189+
190+
// We also want to intercept all import paths inside downloaded
191+
// files and resolve them against the original URL. All of these
192+
// files will be in the "http-url" namespace. Make sure to keep
193+
// the newly resolved URL in the "http-url" namespace so imports
194+
// inside it will also be resolved as URLs recursively.
195+
build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => ({
196+
path: new URL(args.path, args.importer).toString(),
197+
namespace: 'http-url',
198+
}))
199+
200+
// When a URL is loaded, we want to actually download the content
201+
// from the internet. This has just enough logic to be able to
202+
// handle the example import from unpkg.com but in reality this
203+
// would probably need to be more complex.
204+
build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => {
205+
//console.log('loading', args.path);
206+
let contents = await fetch(args.path).then(res => res.text());
207+
//console.log('loaded', args.path, contents);
208+
return { contents }
209+
})
210+
},
211+
}
84212
85-
const executor = new Function(`${content.code}; return exports.Example();`);
86-
console.log('executor', executor, executor());
87-
resultEl.textContent = JSON.stringify(executor());
213+
const start = Date.now();
214+
215+
//return esbuild.transform(body, { loader: 'jsx', format: 'iife', globalName: 'exports', plugins: [exampleOnResolvePlugin] }).then(content => content.code);
216+
return esbuild.build({
217+
bundle: true,
218+
stdin: {
219+
contents: `${prefix}\n${body ?? ""}\n${suffix}`,
220+
loader: 'jsx',
221+
sourcefile: 'main.jsx',
222+
},
223+
write: false,
224+
format: 'iife',
225+
globalName: 'exports',
226+
plugins: [httpPlugin]
227+
})
228+
.then(result => {
229+
const duration = Date.now() - start;
230+
if (result.outputFiles.length > 0) {
231+
return {
232+
code: new TextDecoder().decode(result.outputFiles[0].contents),
233+
duration,
234+
codeBytes: result.outputFiles[0].contents.length
235+
};
236+
} else {
237+
return {
238+
code: "",
239+
duration,
240+
codeBytes: 0
241+
};
242+
}
243+
})
244+
})
245+
.then(({ code, codeBytes, duration }) => {
246+
const executor = new Function(`${code}; return exports.Example();`);
247+
const result = executor();
248+
return new Map()
249+
.set('result', result)
250+
.set('error', '')
251+
.set('esbuildMs', duration.toString() + 'ms')
252+
.set('esbuildBytes', (codeBytes / 1024).toFixed(2) + ' KB')
253+
.set('renderMs', '');
88254
})
89-
.catch((err) => output.getModel().setValue(err.message.replace(/^/gm, '// $&')));
255+
.catch((err) => {
256+
return new Map().set('error', 'Error ' + err.message);
257+
})
258+
.then(data => {
259+
for (const slotEl of resultEl.querySelectorAll('slot[name]')) {
260+
slotEl.textContent = data.get(slotEl.name) || '';
261+
}
262+
htmlOutput.getModel().setValue(data.get('result') || '');
263+
});
264+
90265
/*fetch('/upload', { method: 'POST', body })
91266
.then(async (response) => {
92267
const content = await response.text();
@@ -101,8 +276,15 @@ require(["vs/editor/editor.main"], function () {
101276
});
102277
</script>
103278
<output id=status class="block text-xs opacity-50"></output>
104-
<output id=result class="block text-xs opacity-50"></output>
279+
<output id=result class="block text-xs">
280+
<div class="text-red-500"><slot name=error></slot></div>
281+
<div>esbuild: <slot name=esbuildMs></slot> <slot name=esbuildBytes></slot></div>
282+
<div><slot name=renderMs></slot></div>
283+
</output>
105284
<div class="flex-container" id="container" style="display: flex; min-height: 100vh;">
106285
<div id="input" style="flex: 1;"></div>
107-
<div id="output" style="flex: 1;"></div>
286+
<div class="flex-1 flex flex-col">
287+
<div id="clientApp" style="flex: 1;"></div>
288+
<div id="htmlOutput" style="flex: 1;"></div>
289+
</div>
108290
</div>

0 commit comments

Comments
 (0)