Compare commits
26 Commits
v1.0.0
...
86cee4eaa5
| Author | SHA1 | Date | |
|---|---|---|---|
|
86cee4eaa5
|
|||
|
768f9bce0f
|
|||
|
eb4107a7fd
|
|||
|
264534f479
|
|||
|
c2f6b5d49e
|
|||
|
e97baaad8a
|
|||
|
a0da95fd8b
|
|||
|
d5037dc14e
|
|||
|
e17fcdb401
|
|||
|
77551804f7
|
|||
|
fc18927116
|
|||
|
c557311dbc
|
|||
|
fd4c3750fb
|
|||
|
9ca2ec8859
|
|||
|
4711f7892f
|
|||
|
254c5ab48f
|
|||
|
89cf48e449
|
|||
|
d6d5cd927c
|
|||
|
72ab390756
|
|||
|
92115d3faa
|
|||
|
21eaee22f7
|
|||
|
44e32ce4f7
|
|||
|
6302ec6ab3
|
|||
|
1c45e7250b
|
|||
|
51ee15749e
|
|||
|
bba90c6783
|
@@ -24,6 +24,7 @@ This project is a front-end web UI of Oxygen Toolbox and needs to be used with t
|
||||
# Requires
|
||||
|
||||
- Web Server (e.g. Nginx, Apache httpd)
|
||||
- [API of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-api) (v1.0.0 and later versions)
|
||||
|
||||
# Related projects
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
# 环境要求
|
||||
|
||||
- Web 服务器(如 Nginx, Apache httpd)
|
||||
- [API of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-api) (v1.0.0 及更高版本)
|
||||
|
||||
# 关联项目
|
||||
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "oxygen-ui",
|
||||
"version": "1.0.0-SNAPSHOT",
|
||||
"version": "1.0.1-SNAPSHOT",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "oxygen-ui",
|
||||
"version": "1.0.0-SNAPSHOT",
|
||||
"version": "1.0.1-SNAPSHOT",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.3.7",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
@@ -2153,9 +2153,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "oxygen-ui",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-SNAPSHOT",
|
||||
"description": "Oxygen Toolbox browser version",
|
||||
"author": {
|
||||
"author": {
|
||||
"name": "FatttSnake",
|
||||
"email": "fatttsnake@fatweb.top",
|
||||
"url": "https://fatweb.top"
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
opacity: 0.6;
|
||||
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
opacity: 0.6;
|
||||
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
width: 100%;
|
||||
|
||||
.root-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
||||
@@ -36,6 +36,31 @@ export const createATA = async (): Promise<TypeHelper> => {
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const ts = await import('https://esm.sh/typescript@5.3.3')
|
||||
|
||||
const maxConcurrentRequests = 50
|
||||
let activeRequests = 0
|
||||
const requestQueue: Array<() => void> = []
|
||||
const fetchWithQueue = (input: RequestInfo | URL, init?: RequestInit | undefined) =>
|
||||
new Promise<Response>((resolve, reject) => {
|
||||
const attemptRequest = () => {
|
||||
if (activeRequests < maxConcurrentRequests) {
|
||||
activeRequests++
|
||||
fetch(input, init)
|
||||
.then((response) => resolve(response))
|
||||
.catch((error) => reject(error))
|
||||
.finally(() => {
|
||||
activeRequests--
|
||||
if (requestQueue.length > 0) {
|
||||
requestQueue.shift()?.()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
requestQueue.push(attemptRequest)
|
||||
}
|
||||
}
|
||||
attemptRequest()
|
||||
})
|
||||
|
||||
const ata = setupTypeAcquisition({
|
||||
projectName: 'monaco-ts',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
@@ -43,7 +68,7 @@ export const createATA = async (): Promise<TypeHelper> => {
|
||||
logger: console,
|
||||
fetcher: (input, init) => {
|
||||
try {
|
||||
return fetch(input, init)
|
||||
return fetchWithQueue(input, init)
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
@@ -92,6 +117,7 @@ export const createATA = async (): Promise<TypeHelper> => {
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
acquireType,
|
||||
addListener,
|
||||
removeListener,
|
||||
|
||||
@@ -12,8 +12,8 @@ export const useEditor = () => {
|
||||
const selection = input.options ? input.options.selection : null
|
||||
if (selection) {
|
||||
if (
|
||||
typeof selection.endLineNumber === 'number' &&
|
||||
typeof selection.endColumn === 'number'
|
||||
typeof selection?.endLineNumber === 'number' &&
|
||||
typeof selection?.endColumn === 'number'
|
||||
) {
|
||||
editor.setSelection(selection)
|
||||
editor.revealRangeInCenter(selection, ScrollType.Immediate)
|
||||
|
||||
@@ -32,7 +32,7 @@ const Preview = ({
|
||||
Compiler.compile(files, importMap, entryPoint)
|
||||
.then((result) => {
|
||||
setCompiledCode(
|
||||
`${preExpansionCode}\n${result.outputFiles[0].text}\n${postExpansionCode}`
|
||||
`(()=>{${preExpansionCode}})();\n(()=>{${result.outputFiles[0].text}})();\n(()=>{${postExpansionCode}})();`
|
||||
)
|
||||
setErrorMsg('')
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import MonacoEditor from '@monaco-editor/react'
|
||||
import { Loader } from 'esbuild-wasm'
|
||||
import '@/components/Playground/Output/Transform/transform.scss'
|
||||
import { IFile, ITheme } from '@/components/Playground/shared'
|
||||
import { cssToJs, jsonToJs, addReactImport } from '@/components/Playground/files'
|
||||
import { cssToJsFromFile, jsonToJsFromFile } from '@/components/Playground/files'
|
||||
import Compiler from '@/components/Playground/compiler'
|
||||
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig'
|
||||
|
||||
@@ -16,12 +16,7 @@ const Transform = ({ file, theme }: OutputProps) => {
|
||||
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)
|
||||
Compiler?.transform(code, loader)
|
||||
.then((value) => {
|
||||
setCompiledCode(value.code)
|
||||
setErrorMsg('')
|
||||
@@ -44,10 +39,10 @@ const Transform = ({ file, theme }: OutputProps) => {
|
||||
compile(code, 'jsx')
|
||||
break
|
||||
case 'css':
|
||||
setCompiledCode(cssToJs(file))
|
||||
setCompiledCode(cssToJsFromFile(file))
|
||||
break
|
||||
case 'json':
|
||||
setCompiledCode(jsonToJs(file))
|
||||
setCompiledCode(jsonToJsFromFile(file))
|
||||
break
|
||||
case 'xml':
|
||||
setCompiledCode(code)
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm'
|
||||
import esbuild, {
|
||||
Loader,
|
||||
OnLoadArgs,
|
||||
OnLoadResult,
|
||||
OnResolveArgs,
|
||||
OnResolveResult,
|
||||
Plugin,
|
||||
PluginBuild
|
||||
} from 'esbuild-wasm'
|
||||
import localforage from 'localforage'
|
||||
import axios from 'axios'
|
||||
import { IFiles, IImportMap } from '@/components/Playground/shared'
|
||||
import { cssToJs, jsonToJs, addReactImport } from '@/components/Playground/files'
|
||||
import { IFile, IFiles, IImportMap } from '@/components/Playground/shared'
|
||||
import {
|
||||
addReactImport,
|
||||
cssToJs,
|
||||
cssToJsFromFile,
|
||||
jsonToJs,
|
||||
jsonToJsFromFile
|
||||
} from '@/components/Playground/files'
|
||||
|
||||
class Compiler {
|
||||
private init = false
|
||||
@@ -26,198 +40,272 @@ class Compiler {
|
||||
}
|
||||
}
|
||||
|
||||
transform = (code: string, loader: Loader) =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (this.init) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const timer = setInterval(() => {
|
||||
if (this.init) {
|
||||
clearInterval(timer)
|
||||
resolve()
|
||||
private waitInit = async () => {
|
||||
if (!this.init) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkInit = () => {
|
||||
if (this.init) {
|
||||
resolve()
|
||||
} else {
|
||||
setTimeout(checkInit, 100)
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}).then(() => {
|
||||
return esbuild.transform(code, { loader })
|
||||
})
|
||||
|
||||
compile = (files: IFiles, importMap: IImportMap, entryPoint: string) =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (this.init) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const timer = setInterval(() => {
|
||||
if (this.init) {
|
||||
clearInterval(timer)
|
||||
resolve()
|
||||
}
|
||||
}, 100)
|
||||
}).then(() => {
|
||||
return esbuild.build({
|
||||
bundle: true,
|
||||
entryPoints: [entryPoint],
|
||||
format: 'esm',
|
||||
metafile: true,
|
||||
write: false,
|
||||
plugins: [this.fileResolverPlugin(files, importMap, entryPoint)]
|
||||
checkInit()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
transform = async (code: string, loader: Loader) => {
|
||||
await this.waitInit()
|
||||
return esbuild.transform(code, { loader })
|
||||
}
|
||||
|
||||
compile = async (files: IFiles, importMap: IImportMap, entryPoint: string) => {
|
||||
await this.waitInit()
|
||||
return esbuild.build({
|
||||
bundle: true,
|
||||
entryPoints: [entryPoint],
|
||||
format: 'esm',
|
||||
metafile: true,
|
||||
write: false,
|
||||
plugins: [this.fileResolverPlugin(files, importMap)]
|
||||
})
|
||||
}
|
||||
|
||||
compileCss = async (cssCode: string, basePath: string) => {
|
||||
await this.waitInit()
|
||||
return esbuild.build({
|
||||
bundle: true,
|
||||
entryPoints: [basePath],
|
||||
write: false,
|
||||
plugins: [this.cssCodeResolverPlugin(cssCode, basePath)]
|
||||
})
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
void esbuild.stop()
|
||||
}
|
||||
|
||||
private fileResolverPlugin = (
|
||||
files: IFiles,
|
||||
importMap: IImportMap,
|
||||
entryPoint: string
|
||||
): Plugin => {
|
||||
return {
|
||||
name: 'file-resolver-plugin',
|
||||
setup: (build: PluginBuild) => {
|
||||
build.onResolve({ filter: /.*/ }, (args: esbuild.OnResolveArgs) => {
|
||||
if (entryPoint === args.path) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: args.path
|
||||
}
|
||||
private fileResolverPlugin = (files: IFiles, importMap: IImportMap): Plugin => ({
|
||||
name: 'file-resolver-plugin',
|
||||
setup: (build: PluginBuild) => {
|
||||
build.onResolve({ filter: /.*/ }, (args: OnResolveArgs): OnResolveResult => {
|
||||
if (args.kind === 'entry-point') {
|
||||
return {
|
||||
namespace: 'oxygen',
|
||||
path: args.path
|
||||
}
|
||||
if (args.path.startsWith('./') && files[args.path.substring(2)]) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: args.path.substring(2)
|
||||
}
|
||||
}
|
||||
if (args.path.startsWith('./') && files[`${args.path.substring(2)}.tsx`]) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: `${args.path.substring(2)}.tsx`
|
||||
}
|
||||
}
|
||||
if (args.path.startsWith('./') && files[`${args.path.substring(2)}.jsx`]) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: `${args.path.substring(2)}.jsx`
|
||||
}
|
||||
}
|
||||
if (args.path.startsWith('./') && files[`${args.path.substring(2)}.ts`]) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: `${args.path.substring(2)}.ts`
|
||||
}
|
||||
}
|
||||
if (args.path.startsWith('./') && files[`${args.path.substring(2)}.js`]) {
|
||||
return {
|
||||
namespace: 'OxygenToolbox',
|
||||
path: `${args.path.substring(2)}.js`
|
||||
}
|
||||
}
|
||||
if (/\.\/.*\.css/.test(args.path) && !args.resolveDir) {
|
||||
throw Error(`Css '${args.path}' not found`)
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
if (!path) {
|
||||
throw Error(`Import '${args.path}' not found in Import Map`)
|
||||
}
|
||||
|
||||
}
|
||||
if (/^https?:\/\/.*/.test(args.path)) {
|
||||
return {
|
||||
namespace: 'default',
|
||||
path
|
||||
path: args.path
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs) => {
|
||||
const contents = cssToJs(files[args.path])
|
||||
return {
|
||||
loader: 'js',
|
||||
contents
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs) => {
|
||||
const contents = jsonToJs(files[args.path])
|
||||
return {
|
||||
loader: 'js',
|
||||
contents
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs) => {
|
||||
if (entryPoint === args.path) {
|
||||
if (
|
||||
args.path.startsWith('./') &&
|
||||
(!args.resolveDir.length || args.resolveDir in files)
|
||||
) {
|
||||
const suffix = ['', '.tsx', '.jsx', '.ts', '.js'].find((suffix) => {
|
||||
return files[`${args.path.substring(2)}${suffix}`]
|
||||
})
|
||||
if (suffix !== undefined) {
|
||||
return {
|
||||
loader: 'tsx',
|
||||
contents: addReactImport(files[entryPoint].value)
|
||||
namespace: 'oxygen',
|
||||
path: `${args.path.substring(2)}${suffix}`
|
||||
}
|
||||
}
|
||||
|
||||
if (files[args.path]) {
|
||||
const contents = addReactImport(files[args.path].value)
|
||||
if (args.path.endsWith('.jsx')) {
|
||||
return {
|
||||
loader: 'jsx',
|
||||
contents
|
||||
}
|
||||
}
|
||||
if (args.path.endsWith('.ts')) {
|
||||
return {
|
||||
loader: 'ts',
|
||||
contents
|
||||
}
|
||||
}
|
||||
if (args.path.endsWith('.js')) {
|
||||
return {
|
||||
loader: 'js',
|
||||
contents
|
||||
}
|
||||
}
|
||||
return {
|
||||
loader: 'tsx',
|
||||
contents
|
||||
}
|
||||
}
|
||||
if (['./', '../', '/'].some((prefix) => args.path.startsWith(prefix))) {
|
||||
return {
|
||||
namespace: 'default',
|
||||
path: new URL(args.path, args.resolveDir.substring(1)).href
|
||||
}
|
||||
}
|
||||
|
||||
const cached = await this.fileCache.getItem<esbuild.OnLoadResult>(args.path)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
let path = importMap[args.path]
|
||||
let tempPath = args.path
|
||||
while (!path && tempPath.includes('/')) {
|
||||
tempPath = tempPath.substring(0, tempPath.lastIndexOf('/'))
|
||||
if (importMap[tempPath]) {
|
||||
const suffix = args.path.replace(tempPath, '')
|
||||
const importUrl = new URL(importMap[tempPath])
|
||||
path = `${importUrl.origin}${importUrl.pathname}${suffix}${importUrl.search}`
|
||||
}
|
||||
|
||||
const axiosResponse = await axios.get<string>(args.path)
|
||||
const result: esbuild.OnLoadResult = {
|
||||
loader: 'jsx',
|
||||
contents: axiosResponse.data,
|
||||
resolveDir: (axiosResponse.request as XMLHttpRequest).responseURL
|
||||
}
|
||||
if (!path) {
|
||||
throw Error(`Import '${args.path}' not found in Import Map`)
|
||||
}
|
||||
const pathUrl = new URL(path)
|
||||
const externals = pathUrl.searchParams.get('external')?.split(',') ?? []
|
||||
Object.keys(importMap).forEach((item) => {
|
||||
if (!(item in externals)) {
|
||||
externals.push(item)
|
||||
}
|
||||
|
||||
await this.fileCache.setItem(args.path, result)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
pathUrl.searchParams.set('external', externals.join(','))
|
||||
return {
|
||||
namespace: 'default',
|
||||
path: pathUrl.href
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs): OnLoadResult => {
|
||||
const contents = cssToJsFromFile(files[args.path])
|
||||
return {
|
||||
loader: 'js',
|
||||
contents
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs): OnLoadResult => {
|
||||
const contents = jsonToJsFromFile(files[args.path])
|
||||
return {
|
||||
loader: 'js',
|
||||
contents
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad(
|
||||
{ namespace: 'oxygen', filter: /.*/ },
|
||||
(args: OnLoadArgs): OnLoadResult | undefined => {
|
||||
let file: IFile | undefined
|
||||
|
||||
void ['', '.tsx', '.jsx', '.ts', '.js'].forEach((suffix) => {
|
||||
file = file || files[`${args.path}${suffix}`]
|
||||
})
|
||||
if (file) {
|
||||
return {
|
||||
loader: (() => {
|
||||
switch (file.language) {
|
||||
case 'javascript':
|
||||
return 'jsx'
|
||||
default:
|
||||
return 'tsx'
|
||||
}
|
||||
})(),
|
||||
contents: addReactImport(file.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs): Promise<OnLoadResult> => {
|
||||
const cached = await this.fileCache.getItem<OnLoadResult>(args.path)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const axiosResponse = await axios.get<ArrayBuffer>(args.path, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
const contentType = axiosResponse.headers['content-type'] as string
|
||||
const utf8Decoder = new TextDecoder('utf-8')
|
||||
const result: OnLoadResult = {
|
||||
loader: (() => {
|
||||
if (
|
||||
contentType.includes('javascript') ||
|
||||
contentType.includes('css') ||
|
||||
contentType.includes('json')
|
||||
) {
|
||||
return 'js'
|
||||
}
|
||||
return 'base64'
|
||||
})(),
|
||||
contents: await (async () => {
|
||||
if (contentType.includes('css')) {
|
||||
return cssToJs(
|
||||
(
|
||||
await this.compileCss(
|
||||
utf8Decoder.decode(axiosResponse.data),
|
||||
args.path
|
||||
)
|
||||
).outputFiles[0].text
|
||||
)
|
||||
}
|
||||
if (contentType.includes('json')) {
|
||||
return jsonToJs(utf8Decoder.decode(axiosResponse.data))
|
||||
}
|
||||
return new Uint8Array(axiosResponse.data)
|
||||
})(),
|
||||
resolveDir: args.path
|
||||
}
|
||||
|
||||
await this.fileCache.setItem(args.path, result)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private cssCodeResolverPlugin = (cssCode: string, basePath: string): Plugin => ({
|
||||
name: 'css-code-resolver-plugin',
|
||||
setup: (build: PluginBuild) => {
|
||||
build.onResolve({ filter: /.*/ }, (args: OnResolveArgs): OnResolveResult => {
|
||||
if (args.kind === 'entry-point') {
|
||||
return {
|
||||
namespace: 'default',
|
||||
path: basePath
|
||||
}
|
||||
}
|
||||
return {
|
||||
namespace: 'default',
|
||||
path: args.resolveDir.length
|
||||
? new URL(args.path, args.resolveDir.substring(1)).href
|
||||
: new URL(args.path, basePath).href
|
||||
}
|
||||
})
|
||||
|
||||
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs): Promise<OnLoadResult> => {
|
||||
if (args.path === basePath) {
|
||||
return {
|
||||
loader: 'css',
|
||||
contents: cssCode,
|
||||
resolveDir: basePath
|
||||
}
|
||||
}
|
||||
const cached = await this.fileCache.getItem<OnLoadResult>(args.path)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const axiosResponse = await axios.get<ArrayBuffer>(args.path, {
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
const contentType = axiosResponse.headers['content-type'] as string
|
||||
const utf8Decoder = new TextDecoder('utf-8')
|
||||
const result: OnLoadResult = {
|
||||
loader: (() => {
|
||||
if (contentType.includes('css')) {
|
||||
return 'js'
|
||||
}
|
||||
return 'dataurl'
|
||||
})(),
|
||||
contents: await (async () => {
|
||||
if (contentType.includes('css')) {
|
||||
return cssToJs(
|
||||
(
|
||||
await this.compileCss(
|
||||
utf8Decoder.decode(axiosResponse.data),
|
||||
args.path
|
||||
)
|
||||
).outputFiles[0].text
|
||||
)
|
||||
}
|
||||
return new Uint8Array(axiosResponse.data)
|
||||
})(),
|
||||
resolveDir: args.path
|
||||
}
|
||||
|
||||
await this.fileCache.setItem(args.path, result)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default new Compiler()
|
||||
|
||||
@@ -83,21 +83,25 @@ export const jsToBlob = (code: string) => {
|
||||
return URL.createObjectURL(new Blob([code], { type: 'application/javascript' }))
|
||||
}
|
||||
|
||||
export const jsonToJs = (file: IFile) => {
|
||||
return `export default ${file.value}`
|
||||
export const jsonToJs = (code: string) => {
|
||||
return `export default ${code}`
|
||||
}
|
||||
|
||||
export const cssToJs = (file: IFile) => {
|
||||
export const jsonToJsFromFile = (file: IFile) => {
|
||||
return jsonToJs(file.value)
|
||||
}
|
||||
|
||||
export const cssToJs = (code: string, fileName?: string) => {
|
||||
const randomId = new Date().getTime()
|
||||
return `(() => {
|
||||
let stylesheet = document.getElementById('style_${randomId}_${file.name}');
|
||||
let stylesheet = document.getElementById('style_${randomId}${fileName ? `_${fileName}` : ''}');
|
||||
if (!stylesheet) {
|
||||
stylesheet = document.createElement('style')
|
||||
stylesheet.setAttribute('id', 'style_${randomId}_${file.name}')
|
||||
stylesheet.setAttribute('id', 'style_${randomId}_${fileName ? `_${fileName}` : ''}')
|
||||
document.head.appendChild(stylesheet)
|
||||
}
|
||||
const styles = document.createTextNode(
|
||||
\`${file.value}\`
|
||||
\`${code}\`
|
||||
)
|
||||
stylesheet.innerHTML = ''
|
||||
stylesheet.appendChild(styles)
|
||||
@@ -105,8 +109,12 @@ export const cssToJs = (file: IFile) => {
|
||||
`
|
||||
}
|
||||
|
||||
export const cssToJsFromFile = (file: IFile) => {
|
||||
return cssToJs(file.value, file.name)
|
||||
}
|
||||
|
||||
export const addReactImport = (code: string) => {
|
||||
if (!/import\s+React/g.test(code)) {
|
||||
if (!/^\s*import\s+React\s+/g.test(code)) {
|
||||
return `import React from 'react';\n${code}`
|
||||
}
|
||||
return code
|
||||
@@ -118,18 +126,12 @@ export const tsconfigJsonDiagnosticsOptions: DiagnosticsOptions = {
|
||||
{
|
||||
uri: 'tsconfig.json',
|
||||
fileMatch: ['tsconfig.json'],
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: tsconfigSchema
|
||||
}
|
||||
schema: tsconfigSchema
|
||||
},
|
||||
{
|
||||
uri: 'import-map.json',
|
||||
fileMatch: ['import-map.json'],
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: importMapSchema
|
||||
}
|
||||
schema: importMapSchema
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"imports": {
|
||||
"type": "object",
|
||||
"description": "Import map"
|
||||
}
|
||||
"additionalProperties": {"type": "string"}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ const Playground = ({
|
||||
try {
|
||||
setImportMap(JSON.parse(importMapRaw) as IImportMap)
|
||||
} catch (e) {
|
||||
setImportMap({ imports: {} })
|
||||
setImportMap({})
|
||||
}
|
||||
}
|
||||
if (!tsconfig) {
|
||||
|
||||
@@ -14,20 +14,7 @@ export interface IFiles {
|
||||
[key: string]: IFile
|
||||
}
|
||||
|
||||
export interface ITemplate {
|
||||
name: string
|
||||
tsconfig: ITsconfig
|
||||
importMap: IImportMap
|
||||
files: IFiles
|
||||
}
|
||||
|
||||
export interface ITemplates {
|
||||
[key: string]: ITemplate
|
||||
}
|
||||
|
||||
export interface IImportMap {
|
||||
imports: Record<string, string>
|
||||
}
|
||||
export type IImportMap = Record<string, string>
|
||||
|
||||
export interface ITsconfig {
|
||||
compilerOptions: CompilerOptions
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { ITemplates } from '@/components/Playground/shared'
|
||||
import { ENTRY_FILE_NAME, MAIN_FILE_NAME } from '@/components/Playground/files'
|
||||
|
||||
import baseTsconfig from '@/components/Playground/templates/base/tsconfig.json'
|
||||
import baseImportMap from '@/components/Playground/templates/base/import-map.json'
|
||||
import baseMain from '@/components/Playground/templates/base/main.tsx?raw'
|
||||
import baseApp from '@/components/Playground/templates/base/App.tsx?raw'
|
||||
|
||||
import demoTsconfig from '@/components/Playground/templates/demo/tsconfig.json'
|
||||
import demoImportMap from '@/components/Playground/templates/demo/import-map.json'
|
||||
import demoMain from '@/components/Playground/templates/demo/main.tsx?raw'
|
||||
import demoApp from '@/components/Playground/templates/demo/App.tsx?raw'
|
||||
import demoAppCSS from '@/components/Playground/templates/demo/App.css?raw'
|
||||
|
||||
const templates: ITemplates = {
|
||||
base: {
|
||||
name: '基础',
|
||||
tsconfig: baseTsconfig,
|
||||
importMap: baseImportMap,
|
||||
files: {
|
||||
[ENTRY_FILE_NAME]: {
|
||||
name: ENTRY_FILE_NAME,
|
||||
language: 'typescript',
|
||||
value: baseMain,
|
||||
hidden: true
|
||||
},
|
||||
[MAIN_FILE_NAME]: {
|
||||
name: MAIN_FILE_NAME,
|
||||
language: 'typescript',
|
||||
value: baseApp
|
||||
}
|
||||
}
|
||||
},
|
||||
demo: {
|
||||
name: 'Demo',
|
||||
tsconfig: demoTsconfig,
|
||||
importMap: demoImportMap,
|
||||
files: {
|
||||
[ENTRY_FILE_NAME]: {
|
||||
name: ENTRY_FILE_NAME,
|
||||
language: 'typescript',
|
||||
value: demoMain,
|
||||
hidden: true
|
||||
},
|
||||
[MAIN_FILE_NAME]: {
|
||||
name: MAIN_FILE_NAME,
|
||||
language: 'typescript',
|
||||
value: demoApp
|
||||
},
|
||||
['App.css']: {
|
||||
name: 'App.css',
|
||||
language: 'css',
|
||||
value: demoAppCSS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default templates
|
||||
@@ -1,5 +0,0 @@
|
||||
const App = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18.2.0",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": 7,
|
||||
"useDefineForClassFields": true,
|
||||
"module": 99,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": 2,
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": 4,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"composite": true,
|
||||
"types": ["node"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: rgb(255 255 255 / 87%);
|
||||
text-rendering: optimizelegibility;
|
||||
text-size-adjust: 100%;
|
||||
background-color: #242424;
|
||||
color-scheme: light dark;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
padding: 2rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.6em 1.2em;
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
|
||||
const App = () => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Hello World</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18.2.0",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": 7,
|
||||
"useDefineForClassFields": true,
|
||||
"module": 99,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": 2,
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": 4,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"composite": true,
|
||||
"types": ["node"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -169,18 +169,18 @@ const Base = () => {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
width: '12em',
|
||||
width: '14em',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
<AntdSpace size={'middle'}>
|
||||
{!record.compiled && !Object.keys(hasEdited).length && (
|
||||
{!Object.keys(hasEdited).length && (
|
||||
<Permission operationCode={['system:tool:modify:base']}>
|
||||
<a
|
||||
style={{ color: COLOR_PRODUCTION }}
|
||||
onClick={handleOnCompileBtnClick(record)}
|
||||
>
|
||||
编译
|
||||
{record.compiled ? '重新编译' : '编译'}
|
||||
</a>
|
||||
</Permission>
|
||||
)}
|
||||
@@ -304,7 +304,10 @@ const Base = () => {
|
||||
![
|
||||
IMPORT_MAP_FILE_NAME,
|
||||
TS_CONFIG_FILE_NAME
|
||||
].includes(value)
|
||||
].includes(value) &&
|
||||
!value.endsWith('.d.ts') &&
|
||||
!value.endsWith('.css') &&
|
||||
!value.endsWith('.json')
|
||||
)
|
||||
.map((value) => ({ value, label: value }))}
|
||||
placeholder={'请选择入口文件'}
|
||||
@@ -1097,7 +1100,7 @@ const Base = () => {
|
||||
<Playground.CodeEditor
|
||||
files={editingFiles[editingBaseId]}
|
||||
selectedFileName={editingFileName}
|
||||
onSelectedFileChange={() => {}}
|
||||
onSelectedFileChange={setEditingFileName}
|
||||
onChangeFileContent={handleOnChangeFileContent}
|
||||
showFileSelector={false}
|
||||
tsconfig={tsconfig}
|
||||
|
||||
@@ -27,7 +27,7 @@ const Execute = () => {
|
||||
const output = result.outputFiles[0].text
|
||||
setCompiledCode('')
|
||||
setTimeout(() => {
|
||||
setCompiledCode(`${output}\n${baseDist}`)
|
||||
setCompiledCode(`(() => {${output}})();\n(() => {${baseDist}})();`)
|
||||
}, 100)
|
||||
})
|
||||
.catch((reason) => {
|
||||
|
||||
@@ -1044,7 +1044,7 @@ const Template = () => {
|
||||
<Playground.CodeEditor
|
||||
files={editingFiles[editingTemplateId]}
|
||||
selectedFileName={editingFileName}
|
||||
onSelectedFileChange={() => {}}
|
||||
onSelectedFileChange={setEditingFileName}
|
||||
onChangeFileContent={handleOnChangeFileContent}
|
||||
showFileSelector={false}
|
||||
tsconfig={tsconfig}
|
||||
|
||||
@@ -219,7 +219,7 @@ const Tools = () => {
|
||||
<AntdForm.Item
|
||||
name={'pass'}
|
||||
style={{ marginTop: 10 }}
|
||||
rules={[{ required: true }]}
|
||||
rules={[{ required: true, message: '请选择审核结果' }]}
|
||||
>
|
||||
<AntdRadio.Group>
|
||||
<AntdRadio value={true}>通过</AntdRadio>
|
||||
|
||||
@@ -145,7 +145,7 @@ const Create = () => {
|
||||
.compile(files, importMap, template.entryPoint)
|
||||
.then((result) => {
|
||||
const output = result.outputFiles[0].text
|
||||
setCompiledCode(`${output}\n${baseDist}`)
|
||||
setCompiledCode(`(() => {${output}})();\n(() => {${baseDist}})();`)
|
||||
})
|
||||
.catch((reason) => {
|
||||
void message.error(`编译失败:${reason}`)
|
||||
|
||||
@@ -34,7 +34,7 @@ const View = () => {
|
||||
const output = result.outputFiles[0].text
|
||||
setCompiledCode('')
|
||||
setTimeout(() => {
|
||||
setCompiledCode(`${output}\n${baseDist}`)
|
||||
setCompiledCode(`(() => {${output}})();\n(() => {${baseDist}})();`)
|
||||
}, 100)
|
||||
})
|
||||
.catch((reason) => {
|
||||
@@ -49,7 +49,7 @@ const View = () => {
|
||||
const dist = base64ToStr(toolVo.dist.data!)
|
||||
setCompiledCode('')
|
||||
setTimeout(() => {
|
||||
setCompiledCode(`${dist}\n${baseDist}`)
|
||||
setCompiledCode(`(() => {${dist}})();\n(() => {${baseDist}})();`)
|
||||
}, 100)
|
||||
} catch (e) {
|
||||
void message.error('载入工具失败')
|
||||
|
||||
Reference in New Issue
Block a user