1
+ <!-- See https://www.sanity.io/guides/server-side-rendering-deno-react -->
2
+
1
3
< script src = "https://unpkg.com/monaco-editor@latest/min/vs/loader.js " > </ script >
2
4
3
5
< script type = "module " >
@@ -25,14 +27,104 @@ const theme = window.matchMedia &&
25
27
window.matchMedia('(prefers-color-scheme: dark)').matches
26
28
? 'vs-dark' : undefined;
27
29
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;
30
34
31
35
export function Example() {
32
36
return a + 4 ;
33
37
}
34
38
`.trim();
35
39
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
+
36
128
const types = fetch("https://workers.cloudflare.com/index.d.ts", { cache : 'force-cache' } )
37
129
.then((response) => response.text())
38
130
.catch((err) = > ` // $ { err . message } `);
@@ -56,14 +148,18 @@ require(["vs/editor/editor.main"], function () {
56
148
model: monaco . editor . createModel ( value , 'typescript' , 'ts:worker.ts' ) ,
57
149
value ,
58
150
theme ,
59
- minimap: false
151
+ minimap: {
152
+ enabled: false
153
+ }
60
154
} ) ;
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: '' ,
64
158
theme,
65
159
readOnly: true ,
66
- minimap: false
160
+ minimap: {
161
+ enabled: false
162
+ }
67
163
} ) ;
68
164
const statusEl = document . getElementById ( 'status' ) ;
69
165
const resultEl = document . getElementById ( 'result' ) ;
@@ -78,15 +174,94 @@ require(["vs/editor/editor.main"], function () {
78
174
} ) ;
79
175
80
176
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
+ }
84
212
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', '');
88
254
})
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
+
90
265
/*fetch('/upload', { method: 'POST', body })
91
266
.then(async (response) => {
92
267
const content = await response.text();
@@ -101,8 +276,15 @@ require(["vs/editor/editor.main"], function () {
101
276
} );
102
277
</ script >
103
278
< 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 >
105
284
< div class = "flex-container " id = "container " style = "display: flex; min-height: 100vh; " >
106
285
< 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 >
108
290
</ div >
0 commit comments