Finish Playground

This commit is contained in:
2024-01-12 16:57:25 +08:00
parent dc64b4993c
commit ed4cef1c78
13 changed files with 156 additions and 71 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", msg: e.message });
});
window.addEventListener("load", () => {
window.parent.postMessage({ type: "LOADED", msg: "" });
});
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", msg: "" });
}
});
</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,92 @@
import React, { useRef, useState } from 'react'
import { IFiles, IImportMap } from '@/components/Playground/shared'
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
import { useUpdatedEffect } from '@/util/hooks'
import Compiler from '@/components/Playground/compiler'
import '@/components/Playground/Output/Preview/preview.scss'
interface PreviewProps {
iframeKey: string
files: IFiles
importMap: IImportMap
}
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, importMap }) => {
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
case 'DONE':
setErrorMsg('')
}
}
useUpdatedEffect(() => {
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [])
useUpdatedEffect(() => {
Compiler.compile(files, importMap)
.then((result) => {
if (loaded) {
iframeRef.current?.contentWindow?.postMessage({
type: 'UPDATE',
data: { compiledCode: result.outputFiles[0].text }
} as IMessage)
}
})
.catch((e) => {
setErrorMsg(`编译失败:${e.message}`)
})
}, [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"
/>
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div>
)
}
export default Preview

View File

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

View File

@@ -0,0 +1,80 @@
import React, { useState } from 'react'
import MonacoEditor from '@monaco-editor/react'
import { Loader } from 'esbuild-wasm'
import '@/components/Playground/Output/Transform/transform.scss'
import { useUpdatedEffect } from '@/util/hooks.tsx'
import { IFile, ITheme } from '@/components/Playground/shared.ts'
import Compiler from '@/components/Playground/compiler.ts'
import { cssToJs, jsonToJs } from '@/components/Playground/files.ts'
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig.ts'
import { addReactImport } from '@/components/Playground/utils.ts'
interface OutputProps {
file: IFile
theme?: ITheme
}
const Transform: React.FC<OutputProps> = ({ file, theme }) => {
const [compiledCode, setCompiledCode] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const compile = (code: string, loader: Loader) => {
let _code = code
if (['jsx', 'tsx'].includes(loader)) {
_code = addReactImport(code)
}
Compiler?.transform(_code, loader)
.then((value) => {
setCompiledCode(value.code)
setErrorMsg('')
})
.catch((e) => {
setErrorMsg(`编译失败:${e.message}`)
})
}
useUpdatedEffect(() => {
if (file) {
try {
const code = file.value
switch (file.language) {
case 'typescript':
compile(code, 'tsx')
break
case 'javascript':
compile(code, 'jsx')
break
case 'css':
setCompiledCode(cssToJs(file))
break
case 'json':
setCompiledCode(jsonToJs(file))
break
case 'xml':
setCompiledCode(code)
}
} catch (e) {
console.log(e)
setCompiledCode('')
}
} else {
setCompiledCode('')
}
}, [file, Compiler])
return (
<div data-component={'playground-transform'}>
<MonacoEditor
theme={theme}
language={'javascript'}
value={compiledCode}
options={{ ...MonacoEditorConfig, readOnly: true }}
/>
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div>
)
}
export default Transform

View File

@@ -0,0 +1,3 @@
[data-component=playground-transform] {
position: relative;
}

View File

@@ -0,0 +1,40 @@
import React, { useState } from 'react'
import FlexBox from '@/components/common/FlexBox.tsx'
import FileSelector from '@/components/Playground/CodeEditor/FileSelector'
import Transform from '@/components/Playground/Output/Transform'
import { IFiles, IImportMap } from '@/components/Playground/shared.ts'
import Preview from '@/components/Playground/Output/Preview'
interface OutputProps {
files: IFiles
selectedFileName: string
importMap: IImportMap
}
const Output: React.FC<OutputProps> = ({ files, selectedFileName, importMap }) => {
const [selectedTab, setSelectedTab] = useState('Preview')
return (
<FlexBox data-component={'playground-code-output'}>
<FileSelector
files={{
Preview: { name: 'Preview', language: 'json', value: '' },
Transform: { name: 'Transform', language: 'json', value: '' }
}}
selectedFileName={selectedTab}
onChange={(tabName) => setSelectedTab(tabName)}
readonly
/>
{selectedTab === 'Preview' && (
<Preview
iframeKey={JSON.stringify(importMap)}
files={files}
importMap={importMap}
/>
)}
{selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />}
</FlexBox>
)
}
export default Output