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
3135export function Example() {
3236 return a + 4 ;
3337}
3438`.trim();
3539
40+ const prefix = `
41+ //import React from "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.profiling.min.js/+esm";
42+ //import ReactDOM from "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.profiling.min.js/+esm";
43+ //import ReactDOMServer from "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom-server.profiling.min.js/+esm";
44+ import React, { useReducer , useCallback, useEffect, useState, useMemo } from "https://jspm.dev/react@17.0.2";
45+ import ReactDOM from "https://jspm.dev/react-dom@17.0.2/profiling";
46+ import ReactDOMServer from "https://jspm.dev/react-dom@17.0.2/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+
36128const 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