Optimize compiler. Add preview to Playground.

This commit is contained in:
2024-01-12 13:50:46 +08:00
parent 949e29e7cc
commit dc64b4993c
6 changed files with 213 additions and 52 deletions

View File

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
<!-- es-module-shims -->
</head>
<body>
<script>
window.addEventListener("error", (e) => {
window.parent.postMessage({ type: "ERROR", message: e.message });
});
window.addEventListener("load", () => {
window.parent.postMessage({ type: "LOADED", message: "" });
});
window.addEventListener("message", ({ data }) => {
if (data?.type === "UPDATE") {
// Record old styles that need to be removed
const appStyleElement = document.querySelectorAll("style[id^=\"style_\"]") || [];
// Remove old app
const appSrcElement = document.querySelector("#appSrc");
const oldSrc = appSrcElement.getAttribute("src");
appSrcElement.remove();
// Add new app
const script = document.createElement("script");
script.src = URL.createObjectURL(
new Blob([data.data.compiledCode], {
type: "application/javascript"
})
);
script.id = "appSrc";
script.type = "module";
script.onload = () => {
// Remove old style
appStyleElement.forEach(element => {
element.remove();
});
};
document.body.appendChild(script);
URL.revokeObjectURL(oldSrc);
window.parent.postMessage({ type: "DONE", message: "" });
}
});
</script>
<script type="module" id="appSrc"></script>
<div id="root">
<div
style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
Loading...
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
import React, { useRef, useState } from 'react'
import { IFiles } from '@/components/Playground/shared'
import iframeRaw from '@/components/Playground/Preview/iframe.html?raw'
import { useUpdatedEffect } from '@/util/hooks'
import { IMPORT_MAP_FILE_NAME } from '@/components/Playground/files'
import Compiler from '@/components/Playground/compiler'
import '@/components/Playground/Preview/preview.scss'
interface PreviewProps {
iframeKey: string
files: IFiles
}
interface IMessage {
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE'
msg: string
data: {
compiledCode: string
}
}
const getIframeUrl = (iframeRaw: string) => {
const shimsUrl = '//unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js'
// 判断浏览器是否支持esm 不支持esm就引入es-module-shims
const newIframeRaw =
typeof import.meta === 'undefined'
? iframeRaw.replace(
'<!-- es-module-shims -->',
`<script async src="${shimsUrl}"></script>`
)
: iframeRaw
return URL.createObjectURL(new Blob([newIframeRaw], { type: 'text/html' }))
}
const iframeUrl = getIframeUrl(iframeRaw)
const Preview: React.FC<PreviewProps> = ({ iframeKey, files }) => {
const iframeRef = useRef<HTMLIFrameElement>(null)
const [errorMsg, setErrorMsg] = useState('')
const [loaded, setLoaded] = useState(false)
const handleMessage = ({ data }: { data: IMessage }) => {
const { type, msg } = data
switch (type) {
case 'LOADED':
setLoaded(true)
break
case 'ERROR':
setErrorMsg(msg)
break
default:
setErrorMsg('')
}
}
useEffect(() => {
console.error(errorMsg)
}, [errorMsg])
useUpdatedEffect(() => {
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [])
useUpdatedEffect(() => {
Compiler.compile(files, JSON.parse(files[IMPORT_MAP_FILE_NAME].value))
.then((result) => {
if (loaded) {
iframeRef.current?.contentWindow?.postMessage({
type: 'UPDATE',
data: { compiledCode: result.outputFiles[0].text }
} as IMessage)
}
})
.catch((e) => {
setErrorMsg(e)
})
}, [files, Compiler, loaded])
return (
<div data-component={'playground-preview'}>
<iframe
key={iframeKey}
ref={iframeRef}
src={iframeUrl}
sandbox="allow-popups-to-escape-sandbox allow-scripts allow-popups allow-forms allow-pointer-lock allow-top-navigation allow-modals allow-same-origin"
/>
</div>
)
}
export default Preview

View File

@@ -0,0 +1,7 @@
[data-component=playground-preview] {
display: flex;
iframe {
border: none;
flex: 1;
}
}

View File

@@ -2,27 +2,19 @@ import React from 'react'
import MonacoEditor from '@monaco-editor/react' import MonacoEditor from '@monaco-editor/react'
import { Loader } from 'esbuild-wasm' import { Loader } from 'esbuild-wasm'
import { useUpdatedEffect } from '@/util/hooks' import { useUpdatedEffect } from '@/util/hooks'
import { IFiles, IImportMap, ITheme } from '@/components/Playground/shared' import { IFile, ITheme } from '@/components/Playground/shared'
import Compiler from '@/components/Playground/compiler' import Compiler from '@/components/Playground/compiler'
import { cssToJs, jsonToJs } from '@/components/Playground/files' import { cssToJs, jsonToJs } from '@/components/Playground/files'
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig' import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig'
import { addReactImport } from '@/components/Playground/utils.ts' import { addReactImport } from '@/components/Playground/utils'
interface OutputProps { interface OutputProps {
files: IFiles file: IFile
selectedFileName: string
theme?: ITheme theme?: ITheme
} }
const Preview: React.FC<OutputProps> = ({ files, selectedFileName, theme }) => { const Transform: React.FC<OutputProps> = ({ file, theme }) => {
const compiler = useRef<Compiler>() const [compiledCode, setCompiledCode] = useState('')
const [compileCode, setCompileCode] = useState('')
useUpdatedEffect(() => {
if (!compiler.current) {
compiler.current = new Compiler()
}
}, [])
const compile = (code: string, loader: Loader) => { const compile = (code: string, loader: Loader) => {
let _code = code let _code = code
@@ -30,33 +22,21 @@ const Preview: React.FC<OutputProps> = ({ files, selectedFileName, theme }) => {
_code = addReactImport(code) _code = addReactImport(code)
} }
compiler.current Compiler?.transform(_code, loader)
?.transform(_code, loader)
.then((value) => { .then((value) => {
setCompileCode(value.code) setCompiledCode(value.code)
}) })
.catch((e) => { .catch((e) => {
console.error('编译失败', e) console.error('编译失败', e)
}) })
compiler.current
?.compile(files, {
imports: {
react: 'https://esm.sh/react@18.2.0',
'react-dom/client': 'https://esm.sh/react-dom@18.2.0'
}
})
.then((r) => {
console.log(r)
})
} }
useUpdatedEffect(() => { useUpdatedEffect(() => {
if (files[selectedFileName]) { if (file) {
try { try {
const code = files[selectedFileName].value const code = file.value
switch (files[selectedFileName].language) { switch (file.language) {
case 'typescript': case 'typescript':
compile(code, 'tsx') compile(code, 'tsx')
break break
@@ -64,32 +44,33 @@ const Preview: React.FC<OutputProps> = ({ files, selectedFileName, theme }) => {
compile(code, 'jsx') compile(code, 'jsx')
break break
case 'css': case 'css':
setCompileCode(cssToJs(files[selectedFileName])) setCompiledCode(cssToJs(file))
break break
case 'json': case 'json':
setCompileCode(jsonToJs(files[selectedFileName])) setCompiledCode(jsonToJs(file))
break break
case 'xml': case 'xml':
setCompileCode(code) setCompiledCode(code)
} }
} catch (e) { } catch (e) {
setCompileCode('') console.log(e)
setCompiledCode('')
} }
} else { } else {
setCompileCode('') setCompiledCode('')
} }
}, [files[selectedFileName]]) }, [file, Compiler])
return ( return (
<> <>
<MonacoEditor <MonacoEditor
theme={theme} theme={theme}
language={'javascript'} language={'javascript'}
value={compileCode} value={compiledCode}
options={{ ...MonacoEditorConfig, readOnly: false }} options={{ ...MonacoEditorConfig, readOnly: true }}
/> />
</> </>
) )
} }
export default Preview export default Transform

View File

@@ -1,9 +1,9 @@
import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm' import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm'
import wasm from 'esbuild-wasm/esbuild.wasm?url'
import { IFiles, IImportMap } from '@/components/Playground/shared.ts' import { IFiles, IImportMap } from '@/components/Playground/shared.ts'
import { cssToJs, ENTRY_FILE_NAME, jsonToJs } from '@/components/Playground/files.ts' import { cssToJs, ENTRY_FILE_NAME, jsonToJs } from '@/components/Playground/files.ts'
import localforage from 'localforage' import localforage from 'localforage'
import axios from 'axios' import axios from 'axios'
import { addReactImport } from '@/components/Playground/utils.ts'
class Compiler { class Compiler {
private init = false private init = false
@@ -14,11 +14,13 @@ class Compiler {
constructor() { constructor() {
try { try {
void esbuild.initialize({ worker: true, wasmURL: wasm }).then(() => { void esbuild
.initialize({ worker: true, wasmURL: 'https://esm.sh/esbuild-wasm/esbuild.wasm' })
.finally(() => {
this.init = true this.init = true
}) })
} catch (e) { } catch (e) {
/* empty */ this.init = true
} }
} }
@@ -107,6 +109,24 @@ class Compiler {
} }
} }
if (/^https?:\/\/.*/.test(args.path)) {
return {
namespace: 'default',
path: args.path
}
}
if (
args.path.includes('./') ||
args.path.includes('../') ||
args.path.startsWith('/')
) {
return {
namespace: 'default',
path: new URL(args.path, args.resolveDir.substring(1)).href
}
}
const path = importMap.imports[args.path] const path = importMap.imports[args.path]
if (!path) { if (!path) {
@@ -147,12 +167,12 @@ class Compiler {
if (args.path === ENTRY_FILE_NAME) { if (args.path === ENTRY_FILE_NAME) {
return { return {
loader: 'tsx', loader: 'tsx',
contents: files[ENTRY_FILE_NAME].value contents: addReactImport(files[ENTRY_FILE_NAME].value)
} }
} }
if (files[args.path]) { if (files[args.path]) {
const contents = files[args.path].value const contents = addReactImport(files[args.path].value)
if (args.path.endsWith('.jsx')) { if (args.path.endsWith('.jsx')) {
return { return {
loader: 'jsx', loader: 'jsx',
@@ -187,10 +207,10 @@ class Compiler {
const result: esbuild.OnLoadResult = { const result: esbuild.OnLoadResult = {
loader: 'jsx', loader: 'jsx',
contents: data, contents: data,
resolveDir: new URL('./', request.responseURL).pathname resolveDir: request.responseURL
} }
await this.fileCache.setItem(args.path, request) await this.fileCache.setItem(args.path, result)
return result return result
}) })
@@ -199,4 +219,4 @@ class Compiler {
} }
} }
export default Compiler export default new Compiler()

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import CodeEditor from '@/components/Playground/CodeEditor' import CodeEditor from '@/components/Playground/CodeEditor'
import { initFiles, MAIN_FILE_NAME } from '@/components/Playground/files' import { IMPORT_MAP_FILE_NAME, initFiles, MAIN_FILE_NAME } from '@/components/Playground/files'
import { IFiles } from '@/components/Playground/shared' import { IFiles } from '@/components/Playground/shared'
import FitFullscreen from '@/components/common/FitFullscreen' import FitFullscreen from '@/components/common/FitFullscreen'
import FlexBox from '@/components/common/FlexBox' import FlexBox from '@/components/common/FlexBox'
import Transform from '@/components/Playground/Transform' import Preview from '@/components/Playground/Preview'
const OnlineEditor: React.FC = () => { const OnlineEditor: React.FC = () => {
const [files, setFiles] = useState<IFiles>(initFiles) const [files, setFiles] = useState<IFiles>(initFiles)
@@ -23,7 +23,7 @@ const OnlineEditor: React.FC = () => {
onRenameFile={(_, __, files) => setFiles(files)} onRenameFile={(_, __, files) => setFiles(files)}
onChangeFileContent={(_, __, files) => setFiles(files)} onChangeFileContent={(_, __, files) => setFiles(files)}
/> />
<Transform files={files} selectedFileName={selectedFileName} /> <Preview iframeKey={files[IMPORT_MAP_FILE_NAME].value} files={files} />
</FlexBox> </FlexBox>
</FitFullscreen> </FitFullscreen>
</> </>