Optimize compiler. Add preview to Playground.
This commit is contained in:
58
src/components/Playground/Preview/iframe.html
Normal file
58
src/components/Playground/Preview/iframe.html
Normal 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>
|
||||||
95
src/components/Playground/Preview/index.tsx
Normal file
95
src/components/Playground/Preview/index.tsx
Normal 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
|
||||||
7
src/components/Playground/Preview/preview.scss
Normal file
7
src/components/Playground/Preview/preview.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[data-component=playground-preview] {
|
||||||
|
display: flex;
|
||||||
|
iframe {
|
||||||
|
border: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user