From 71052009aec6f1cc43e594d6a04f3e4429befefa Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Wed, 10 Jan 2024 18:15:17 +0800 Subject: [PATCH] Add compiler --- package-lock.json | 14 +++- package.json | 1 + src/components/Playground/Preview/index.tsx | 78 +++++++++++++++++++++ src/components/Playground/compiler.ts | 30 ++++++++ src/components/Playground/files.ts | 36 +++++++++- src/pages/OnlineEditor.tsx | 22 ++++-- 6 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 src/components/Playground/Preview/index.tsx create mode 100644 src/components/Playground/compiler.ts diff --git a/package-lock.json b/package-lock.json index 93cb57b..5e6af34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "^1.6.2", "dayjs": "^1.11.10", "echarts": "^5.4.3", + "esbuild-wasm": "^0.19.11", "fast-deep-equal": "^3.1.3", "fflate": "^0.8.1", "jwt-decode": "^4.0.0", @@ -2228,7 +2229,7 @@ "resolved": "https://registry.npmmirror.com/@typescript/ata/-/ata-0.9.4.tgz", "integrity": "sha512-PaJ16WouPV/SaA+c0tnOKIqYq24+m93ipl/e0Dkxuianer+ibc5b0/6ZgfCFF8J7QEp57dySMSP9nWOFaCfJnw==", "peerDependencies": { - "typescript": "^4.4.4" + "typescript": ">=4.4.4 <6.0.0" } }, "node_modules/@ungap/structured-clone": { @@ -3302,6 +3303,17 @@ "@esbuild/win32-x64": "0.19.8" } }, + "node_modules/esbuild-wasm": { + "version": "0.19.11", + "resolved": "https://registry.npmmirror.com/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz", + "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", diff --git a/package.json b/package.json index 9f82e44..6d72dbf 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "axios": "^1.6.2", "dayjs": "^1.11.10", "echarts": "^5.4.3", + "esbuild-wasm": "^0.19.11", "fast-deep-equal": "^3.1.3", "fflate": "^0.8.1", "jwt-decode": "^4.0.0", diff --git a/src/components/Playground/Preview/index.tsx b/src/components/Playground/Preview/index.tsx new file mode 100644 index 0000000..e9473c2 --- /dev/null +++ b/src/components/Playground/Preview/index.tsx @@ -0,0 +1,78 @@ +import React, { useRef, useState } from 'react' +import { IFiles } from '@/components/Playground/shared.ts' +import { useUpdatedEffect } from '@/util/hooks.tsx' +import Compiler from '@/components/Playground/compiler.ts' +import { Loader } from 'esbuild-wasm' +import { cssToJs, jsonToJs } from '@/components/Playground/files.ts' + +interface OutputProps { + files: IFiles +} + +const Preview: React.FC = ({ files }) => { + const compiler = useRef() + const [compileCode, setCompileCode] = useState('') + const [fileName, setFileName] = useState('main.tsx') + + const handleOnChange = (e: React.ChangeEvent) => { + setFileName(e.target.value) + } + + useUpdatedEffect(() => { + if (!compiler.current) { + compiler.current = new Compiler() + } + }, []) + + useUpdatedEffect(() => { + if (files[fileName]) { + try { + const file = files[fileName] + + let loader: Loader + let code = file.value + + switch (file.language) { + case 'typescript': + loader = 'tsx' + break + case 'javascript': + loader = 'jsx' + break + case 'css': + code = cssToJs(file) + loader = 'js' + break + case 'json': + code = jsonToJs(file) + loader = 'js' + break + case 'xml': + loader = 'default' + } + + compiler.current + ?.transform(code, loader) + .then((value) => { + setCompileCode(value.code) + }) + .catch((e) => { + console.error('编译失败', e) + }) + } catch (e) { + setCompileCode('') + } + } else { + setCompileCode('') + } + }, [fileName, files]) + + return ( + <> + + + + ) +} + +export default Preview diff --git a/src/components/Playground/compiler.ts b/src/components/Playground/compiler.ts new file mode 100644 index 0000000..ece9440 --- /dev/null +++ b/src/components/Playground/compiler.ts @@ -0,0 +1,30 @@ +import esbuild, { Loader } from 'esbuild-wasm' +import wasm from 'esbuild-wasm/esbuild.wasm?url' + +class Compiler { + private init = false + + constructor() { + try { + void esbuild.initialize({ worker: true, wasmURL: wasm }).then(() => { + this.init = true + }) + } catch (e) { + throw e + } + } + + transform = (code: string, loader: Loader) => + new Promise((resolve) => { + const timer = setInterval(() => { + if (this.init) { + clearInterval(timer) + resolve(true) + } + }, 100) + }).then(() => { + return esbuild.transform(code, { loader }) + }) +} + +export default Compiler diff --git a/src/components/Playground/files.ts b/src/components/Playground/files.ts index c1d441c..ab47d75 100644 --- a/src/components/Playground/files.ts +++ b/src/components/Playground/files.ts @@ -3,7 +3,7 @@ import importMap from '@/components/Playground/template/import-map.json?raw' import AppCss from '@/components/Playground/template/src/App.css?raw' import App from '@/components/Playground/template/src/App.tsx?raw' import main from '@/components/Playground/template/src/main.tsx?raw' -import { IFiles } from '@/components/Playground/shared' +import { IFile, IFiles } from '@/components/Playground/shared' export const MAIN_FILE_NAME = 'App.tsx' export const IMPORT_MAP_FILE_NAME = 'import-map.json' @@ -51,6 +51,40 @@ export const getFilesFromUrl = () => { return files } +export const getModuleFile = (files: IFiles, moduleName: string) => { + let _moduleName = moduleName.split('./').pop() || '' + if (!_moduleName.includes('.')) { + const realModuleName = Object.keys(files).find((key) => + key.split('.').includes(_moduleName) + ) + if (realModuleName) _moduleName = realModuleName + } + return files[_moduleName] +} + +export const jsonToJs = (file: IFile) => { + const js = `export default ${file.value}` + return URL.createObjectURL(new Blob([js], { type: 'application/javascript' })) +} + +export const cssToJs = (file: IFile) => { + const randomId = new Date().getTime() + const js = ` + (() => { + let stylesheet = document.getElementById('style_${randomId}_${file.name}'); + if (!stylesheet) { + stylesheet = document.createElement('style') + stylesheet.setAttribute('id', 'style_${randomId}_${file.name}') + document.head.appendChild(stylesheet) + } + const styles = document.createTextNode(\`${file.value}\`) + stylesheet.innerHTML = '' + stylesheet.appendChild(styles) + })() + ` + return URL.createObjectURL(new Blob([js], { type: 'application/javascript' })) +} + export const initFiles: IFiles = getFilesFromUrl() || { [ENTRY_FILE_NAME]: { name: ENTRY_FILE_NAME, diff --git a/src/pages/OnlineEditor.tsx b/src/pages/OnlineEditor.tsx index 1f45a3c..aacaff1 100644 --- a/src/pages/OnlineEditor.tsx +++ b/src/pages/OnlineEditor.tsx @@ -2,19 +2,27 @@ import React from 'react' import CodeEditor from '@/components/Playground/CodeEditor' import { initFiles } from '@/components/Playground/files' import { IFiles } from '@/components/Playground/shared' +import FitFullscreen from '@/components/common/FitFullscreen.tsx' +import FlexBox from '@/components/common/FlexBox.tsx' +import Preview from '@/components/Playground/Preview' const OnlineEditor: React.FC = () => { const [files, setFiles] = useState(initFiles) return ( <> - setFiles(files)} - onRemoveFile={(_, files) => setFiles(files)} - onRenameFile={(_, __, files) => setFiles(files)} - onChangeFileContent={(_, __, files) => setFiles(files)} - /> + + + setFiles(files)} + onRemoveFile={(_, files) => setFiles(files)} + onRenameFile={(_, __, files) => setFiles(files)} + onChangeFileContent={(_, __, files) => setFiles(files)} + /> + + + ) }