34 Commits

Author SHA1 Message Date
47d9aa22d8 Merge pull request #20 from FatttSnake/release/1.0.2
v1.0.2
2024-10-09 22:41:47 +08:00
e601a96288 Build(package.json): Update version from 1.0.2-SNAPSHOT to 1.0.2 2024-10-09 22:40:03 +08:00
d63473b3e3 Merge pull request #19 from FatttSnake/refactor/readme
Update README
2024-10-09 22:38:12 +08:00
e2cedef75c Refactor(README): Update requires 2024-10-09 22:34:09 +08:00
361e225244 Merge pull request #18 from FatttSnake/refactor/compiler
Change esbuild transform and compile target to es2015
2024-09-30 16:53:36 +08:00
aad0b9eb3b Merge pull request #17 from FatttSnake/fix/style
Fixed incorrect style that card content exceeds card size
2024-09-30 16:51:42 +08:00
96a9714cdd Refactor(compiler): Change esbuild transform and compile target to es2015 2024-09-30 16:47:06 +08:00
3812ffd18f Style(ToolCard): Fixed incorrect style that card content exceeds card size 2024-09-20 11:53:57 +08:00
6f0ebfe358 Merge pull request #16 from FatttSnake/prepare/1.0.1
Update version from 1.0.1-SNAPSHOT to 1.0.2-SNAPSHOT
2024-09-19 16:44:47 +08:00
0d3dd30591 Merge pull request #15 from FatttSnake/release/1.0.1
v1.0.1
2024-09-19 16:42:16 +08:00
ac8d21c063 Build(package.json): Update version from 1.0.1 to 1.0.2-SNAPSHOT 2024-09-19 16:38:50 +08:00
ebaa4d8851 Build(package.json): Update version from 1.0.1-SNAPSHOT to 1.0.1 2024-09-19 16:37:43 +08:00
86cee4eaa5 Merge pull request #14 from FatttSnake/refactor/editor
Fix draggable mask incorrect style
2024-09-19 13:59:52 +08:00
768f9bce0f Style(CodePage): Fix draggable mask incorrect style 2024-09-19 13:58:43 +08:00
eb4107a7fd Merge pull request #13 from FatttSnake/refactor/editor
Fix "net::ERR_INSUFFICIENT_RESOURCES" exception
2024-09-19 10:00:36 +08:00
264534f479 Fix(Management): Fix base and template management unable to jump file 2024-09-19 09:59:04 +08:00
c2f6b5d49e Fix(editor): Fix "net::ERR_INSUFFICIENT_RESOURCES" exception when the editor loads a large number of libraries 2024-09-19 09:45:24 +08:00
e97baaad8a Merge pull request #12 from FatttSnake/refactor/tool-management
Fixed some UI issues
2024-09-15 18:45:21 +08:00
a0da95fd8b Merge pull request #11 from FatttSnake/refactor/compiler
Add support for loading css from ImportMap and loading binary file
2024-09-15 18:44:48 +08:00
d5037dc14e Docs(README): Add API version requirement 2024-09-15 18:31:23 +08:00
e17fcdb401 Refactor(BaseManagement): Optimize the filter of entry file 2024-09-15 18:27:54 +08:00
77551804f7 Refactor(ToolManagement): Optimize the prompt for required review option 2024-09-15 18:22:02 +08:00
fc18927116 Style(close-editor-btn): Fix the bug of editor cover the editor close button 2024-09-15 15:05:19 +08:00
c557311dbc Refactor(json schema): Optimize import map json schema 2024-09-15 14:54:10 +08:00
fd4c3750fb Refactor(template): Remove unused templates 2024-09-15 13:10:45 +08:00
9ca2ec8859 Refactor(BaseManagement): Support recompile 2024-09-15 13:07:32 +08:00
4711f7892f Refactor(compiler): Support load css from ImportMap and load binary file 2024-09-15 10:54:14 +08:00
254c5ab48f Merge pull request #10 from FatttSnake/refactor/compiler
Fix bug in compiler handling of dependency imports
2024-09-14 00:40:10 +08:00
89cf48e449 Refactor(compiler): Optimize the logic of resolve import
Automatically externalize packages defined in importMap.
Solve the problems caused by multiple instances of packages such as React.

Closes #9
2024-09-13 16:16:01 +08:00
d6d5cd927c Refactor(Transform): Remove auto add react import when transform 2024-09-13 16:15:58 +08:00
72ab390756 Refactor(files-util): Optimize the regular expression to match "import react" 2024-09-13 16:15:55 +08:00
4570a59455 Merge pull request #3 from FatttSnake/1.0.0
v1.0.0
2024-09-06 15:17:40 +08:00
FatttSnake
d0121b126b Merge pull request 'v1.0-230926' (#27) from dev into master
Reviewed-on: FatttSnake/fatweb-ui#27
2023-09-26 11:06:04 +08:00
FatttSnake
5eaae8eef7 Merge pull request 'Merge from dev branch to master' (#2) from dev into master
Reviewed-on: FatttSnake/fatweb-ui#2
2023-09-04 23:26:46 +08:00
32 changed files with 1138 additions and 1209 deletions

View File

@@ -24,6 +24,7 @@ This project is a front-end web UI of Oxygen Toolbox and needs to be used with t
# Requires # Requires
- Web Server (e.g. Nginx, Apache httpd) - Web Server (e.g. Nginx, Apache httpd)
- [API of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-api) (v1.0.0 or later versions)
# Related projects # Related projects

View File

@@ -24,6 +24,7 @@
# 环境要求 # 环境要求
- Web 服务器(如 Nginx, Apache httpd - Web 服务器(如 Nginx, Apache httpd
- [API of Oxygen Toolbox](https://github.com/FatttSnake/oxygen-api) (v1.0.0 或更高版本)
# 关联项目 # 关联项目

View File

@@ -2,7 +2,7 @@
"name": "oxygen-ui", "name": "oxygen-ui",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "1.0.1-SNAPSHOT", "version": "1.0.2",
"description": "Oxygen Toolbox browser version", "description": "Oxygen Toolbox browser version",
"author": { "author": {
"name": "FatttSnake", "name": "FatttSnake",

View File

@@ -78,6 +78,7 @@
.author { .author {
display: flex; display: flex;
margin-top: auto; margin-top: auto;
padding-top: 8px;
flex-direction: row; flex-direction: row;
justify-content: end; justify-content: end;
padding-bottom: 10px; padding-bottom: 10px;

View File

@@ -38,6 +38,7 @@
opacity: 0.6; opacity: 0.6;
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2); box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
cursor: pointer; cursor: pointer;
z-index: 1000;
} }
} }
} }

View File

@@ -38,6 +38,7 @@
opacity: 0.6; opacity: 0.6;
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2); box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
cursor: pointer; cursor: pointer;
z-index: 1000;
} }
} }
} }

View File

@@ -6,6 +6,7 @@
width: 100%; width: 100%;
.root-content { .root-content {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -13,7 +13,7 @@
> .card-box, > div { > .card-box, > div {
width: 180px; width: 180px;
height: 290px; min-height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -102,7 +102,7 @@
> .card-box, > div { > .card-box, > div {
width: 180px; width: 180px;
height: 290px; min-height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
} }
} }

View File

@@ -28,7 +28,7 @@
> div { > div {
width: 180px; width: 180px;
height: 290px; min-height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
} }

View File

@@ -70,7 +70,7 @@
> div { > div {
width: 180px; width: 180px;
height: 290px; min-height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
} }

View File

@@ -36,6 +36,31 @@ export const createATA = async (): Promise<TypeHelper> => {
// @ts-expect-error // @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const ts = await import('https://esm.sh/typescript@5.3.3') 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({ const ata = setupTypeAcquisition({
projectName: 'monaco-ts', projectName: 'monaco-ts',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -43,7 +68,7 @@ export const createATA = async (): Promise<TypeHelper> => {
logger: console, logger: console,
fetcher: (input, init) => { fetcher: (input, init) => {
try { try {
return fetch(input, init) return fetchWithQueue(input, init)
} catch (error) { } catch (error) {
console.error('Error fetching data:', error) console.error('Error fetching data:', error)
} }
@@ -92,6 +117,7 @@ export const createATA = async (): Promise<TypeHelper> => {
} }
return { return {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
acquireType, acquireType,
addListener, addListener,
removeListener, removeListener,

View File

@@ -12,8 +12,8 @@ export const useEditor = () => {
const selection = input.options ? input.options.selection : null const selection = input.options ? input.options.selection : null
if (selection) { if (selection) {
if ( if (
typeof selection.endLineNumber === 'number' && typeof selection?.endLineNumber === 'number' &&
typeof selection.endColumn === 'number' typeof selection?.endColumn === 'number'
) { ) {
editor.setSelection(selection) editor.setSelection(selection)
editor.revealRangeInCenter(selection, ScrollType.Immediate) editor.revealRangeInCenter(selection, ScrollType.Immediate)

View File

@@ -2,7 +2,7 @@ import MonacoEditor from '@monaco-editor/react'
import { Loader } from 'esbuild-wasm' import { Loader } from 'esbuild-wasm'
import '@/components/Playground/Output/Transform/transform.scss' import '@/components/Playground/Output/Transform/transform.scss'
import { IFile, ITheme } from '@/components/Playground/shared' 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 Compiler from '@/components/Playground/compiler'
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig' import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig'
@@ -16,12 +16,7 @@ const Transform = ({ file, theme }: OutputProps) => {
const [errorMsg, setErrorMsg] = useState('') const [errorMsg, setErrorMsg] = useState('')
const compile = (code: string, loader: Loader) => { const compile = (code: string, loader: Loader) => {
let _code = code Compiler?.transform(code, loader)
if (['jsx', 'tsx'].includes(loader)) {
_code = addReactImport(code)
}
Compiler?.transform(_code, loader)
.then((value) => { .then((value) => {
setCompiledCode(value.code) setCompiledCode(value.code)
setErrorMsg('') setErrorMsg('')
@@ -44,10 +39,10 @@ const Transform = ({ file, theme }: OutputProps) => {
compile(code, 'jsx') compile(code, 'jsx')
break break
case 'css': case 'css':
setCompiledCode(cssToJs(file)) setCompiledCode(cssToJsFromFile(file))
break break
case 'json': case 'json':
setCompiledCode(jsonToJs(file)) setCompiledCode(jsonToJsFromFile(file))
break break
case 'xml': case 'xml':
setCompiledCode(code) setCompiledCode(code)

View File

@@ -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 localforage from 'localforage'
import axios from 'axios' import axios from 'axios'
import { IFiles, IImportMap } from '@/components/Playground/shared' import { IFile, IFiles, IImportMap } from '@/components/Playground/shared'
import { addReactImport, cssToJs, jsonToJs } from '@/components/Playground/files' import {
addReactImport,
cssToJs,
cssToJsFromFile,
jsonToJs,
jsonToJsFromFile
} from '@/components/Playground/files'
class Compiler { class Compiler {
private init = false private init = false
@@ -26,97 +40,63 @@ class Compiler {
} }
} }
transform = (code: string, loader: Loader) => private waitInit = async () => {
new Promise<void>((resolve) => { if (!this.init) {
await new Promise<void>((resolve) => {
const checkInit = () => {
if (this.init) { if (this.init) {
resolve() resolve()
return } else {
setTimeout(checkInit, 100)
} }
const timer = setInterval(() => {
if (this.init) {
clearInterval(timer)
resolve()
} }
}, 100) checkInit()
}).then(() => {
return esbuild.transform(code, { loader })
}) })
}
}
compile = (files: IFiles, importMap: IImportMap, entryPoint: string) => transform = async (code: string, loader: Loader) => {
new Promise<void>((resolve) => { await this.waitInit()
if (this.init) { return esbuild.transform(code, { loader, target: 'es2015' })
resolve()
return
} }
const timer = setInterval(() => {
if (this.init) { compile = async (files: IFiles, importMap: IImportMap, entryPoint: string) => {
clearInterval(timer) await this.waitInit()
resolve()
}
}, 100)
}).then(() => {
return esbuild.build({ return esbuild.build({
bundle: true, bundle: true,
entryPoints: [entryPoint], entryPoints: [entryPoint],
format: 'esm', format: 'esm',
target: 'es2015',
metafile: true, metafile: true,
write: false, write: false,
plugins: [this.fileResolverPlugin(files, importMap, entryPoint)] 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 = () => { stop = () => {
void esbuild.stop() void esbuild.stop()
} }
private fileResolverPlugin = ( private fileResolverPlugin = (files: IFiles, importMap: IImportMap): Plugin => ({
files: IFiles,
importMap: IImportMap,
entryPoint: string
): Plugin => ({
name: 'file-resolver-plugin', name: 'file-resolver-plugin',
setup: (build: PluginBuild) => { setup: (build: PluginBuild) => {
build.onResolve({ filter: /.*/ }, (args: esbuild.OnResolveArgs) => { build.onResolve({ filter: /.*/ }, (args: OnResolveArgs): OnResolveResult => {
if (entryPoint === args.path) { if (args.kind === 'entry-point') {
return { return {
namespace: 'OxygenToolbox', namespace: 'oxygen',
path: args.path 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)) { if (/^https?:\/\/.*/.test(args.path)) {
return { return {
namespace: 'default', namespace: 'default',
@@ -125,94 +105,200 @@ class Compiler {
} }
if ( if (
args.path.includes('./') || args.path.startsWith('./') &&
args.path.includes('../') || (!args.resolveDir.length || args.resolveDir in files)
args.path.startsWith('/')
) { ) {
const suffix = ['', '.tsx', '.jsx', '.ts', '.js'].find((suffix) => {
return files[`${args.path.substring(2)}${suffix}`]
})
if (suffix !== undefined) {
return {
namespace: 'oxygen',
path: `${args.path.substring(2)}${suffix}`
}
}
}
if (['./', '../', '/'].some((prefix) => args.path.startsWith(prefix))) {
return { return {
namespace: 'default', namespace: 'default',
path: new URL(args.path, args.resolveDir.substring(1)).href path: new URL(args.path, args.resolveDir.substring(1)).href
} }
} }
let path = importMap.imports[args.path] let path = importMap[args.path]
let tempPath = args.path let tempPath = args.path
while (!path && tempPath.includes('/')) { while (!path && tempPath.includes('/')) {
tempPath = tempPath.substring(0, tempPath.lastIndexOf('/')) tempPath = tempPath.substring(0, tempPath.lastIndexOf('/'))
path = args.path.replace(tempPath, importMap.imports[tempPath]) if (importMap[tempPath]) {
const suffix = args.path.replace(tempPath, '')
const importUrl = new URL(importMap[tempPath])
path = `${importUrl.origin}${importUrl.pathname}${suffix}${importUrl.search}`
}
} }
if (!path) { if (!path) {
throw Error(`Import '${args.path}' not found in Import Map`) 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)
}
})
pathUrl.searchParams.set('external', externals.join(','))
return { return {
namespace: 'default', namespace: 'default',
path path: pathUrl.href
} }
}) })
build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs) => { build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs): OnLoadResult => {
const contents = cssToJs(files[args.path]) const contents = cssToJsFromFile(files[args.path])
return { return {
loader: 'js', loader: 'js',
contents contents
} }
}) })
build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs) => { build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs): OnLoadResult => {
const contents = jsonToJs(files[args.path]) const contents = jsonToJsFromFile(files[args.path])
return { return {
loader: 'js', loader: 'js',
contents contents
} }
}) })
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs) => { build.onLoad(
if (entryPoint === args.path) { { namespace: 'oxygen', filter: /.*/ },
return { (args: OnLoadArgs): OnLoadResult | undefined => {
loader: 'tsx', let file: IFile | undefined
contents: addReactImport(files[entryPoint].value)
}
}
if (files[args.path]) { void ['', '.tsx', '.jsx', '.ts', '.js'].forEach((suffix) => {
const contents = addReactImport(files[args.path].value) file = file || files[`${args.path}${suffix}`]
if (args.path.endsWith('.jsx')) { })
if (file) {
return { return {
loader: 'jsx', loader: (() => {
contents switch (file.language) {
case 'javascript':
return 'jsx'
default:
return 'tsx'
}
})(),
contents: addReactImport(file.value)
} }
} }
if (args.path.endsWith('.ts')) {
return {
loader: 'ts',
contents
}
}
if (args.path.endsWith('.js')) {
return {
loader: 'js',
contents
}
}
return {
loader: 'tsx',
contents
}
} }
)
const cached = await this.fileCache.getItem<esbuild.OnLoadResult>(args.path) build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs): Promise<OnLoadResult> => {
const cached = await this.fileCache.getItem<OnLoadResult>(args.path)
if (cached) { if (cached) {
return cached return cached
} }
const axiosResponse = await axios.get<string>(args.path) const axiosResponse = await axios.get<ArrayBuffer>(args.path, {
const result: esbuild.OnLoadResult = { responseType: 'arraybuffer'
loader: 'jsx', })
contents: axiosResponse.data, const contentType = axiosResponse.headers['content-type'] as string
resolveDir: (axiosResponse.request as XMLHttpRequest).responseURL 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) await this.fileCache.setItem(args.path, result)

View File

@@ -83,21 +83,25 @@ export const jsToBlob = (code: string) => {
return URL.createObjectURL(new Blob([code], { type: 'application/javascript' })) return URL.createObjectURL(new Blob([code], { type: 'application/javascript' }))
} }
export const jsonToJs = (file: IFile) => { export const jsonToJs = (code: string) => {
return `export default ${file.value}` 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() const randomId = new Date().getTime()
return `(() => { return `(() => {
let stylesheet = document.getElementById('style_${randomId}_${file.name}'); let stylesheet = document.getElementById('style_${randomId}${fileName ? `_${fileName}` : ''}');
if (!stylesheet) { if (!stylesheet) {
stylesheet = document.createElement('style') stylesheet = document.createElement('style')
stylesheet.setAttribute('id', 'style_${randomId}_${file.name}') stylesheet.setAttribute('id', 'style_${randomId}_${fileName ? `_${fileName}` : ''}')
document.head.appendChild(stylesheet) document.head.appendChild(stylesheet)
} }
const styles = document.createTextNode( const styles = document.createTextNode(
\`${file.value}\` \`${code}\`
) )
stylesheet.innerHTML = '' stylesheet.innerHTML = ''
stylesheet.appendChild(styles) 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) => { 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 `import React from 'react';\n${code}`
} }
return code return code
@@ -118,18 +126,12 @@ export const tsconfigJsonDiagnosticsOptions: DiagnosticsOptions = {
{ {
uri: 'tsconfig.json', uri: 'tsconfig.json',
fileMatch: ['tsconfig.json'], fileMatch: ['tsconfig.json'],
schema: { schema: tsconfigSchema
type: 'object',
properties: tsconfigSchema
}
}, },
{ {
uri: 'import-map.json', uri: 'import-map.json',
fileMatch: ['import-map.json'], fileMatch: ['import-map.json'],
schema: { schema: importMapSchema
type: 'object',
properties: importMapSchema
}
} }
] ]
} }

View File

@@ -1,6 +1,4 @@
{ {
"imports": {
"type": "object", "type": "object",
"description": "Import map" "additionalProperties": {"type": "string"}
}
} }

View File

@@ -34,7 +34,7 @@ const Playground = ({
try { try {
setImportMap(JSON.parse(importMapRaw) as IImportMap) setImportMap(JSON.parse(importMapRaw) as IImportMap)
} catch (e) { } catch (e) {
setImportMap({ imports: {} }) setImportMap({})
} }
} }
if (!tsconfig) { if (!tsconfig) {

View File

@@ -14,20 +14,7 @@ export interface IFiles {
[key: string]: IFile [key: string]: IFile
} }
export interface ITemplate { export type IImportMap = Record<string, string>
name: string
tsconfig: ITsconfig
importMap: IImportMap
files: IFiles
}
export interface ITemplates {
[key: string]: ITemplate
}
export interface IImportMap {
imports: Record<string, string>
}
export interface ITsconfig { export interface ITsconfig {
compilerOptions: CompilerOptions compilerOptions: CompilerOptions

View File

@@ -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

View File

@@ -1,5 +0,0 @@
const App = () => {
return <></>
}
export default App

View File

@@ -1,6 +0,0 @@
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0"
}
}

View File

@@ -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>
)

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -1,6 +0,0 @@
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0"
}
}

View File

@@ -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>
)

View File

@@ -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
}
}

View File

@@ -1,4 +1,6 @@
{ {
"type": "object",
"properties": {
"compilerOptions": { "compilerOptions": {
"type": "object", "type": "object",
"description": "Instructs the TypeScript compiler how to compile .ts files.", "description": "Instructs the TypeScript compiler how to compile .ts files.",
@@ -71,7 +73,10 @@
}, },
"declarationDir": { "declarationDir": {
"description": "Specify the output directory for generated declaration files.", "description": "Specify the output directory for generated declaration files.",
"type": ["string", "null"], "type": [
"string",
"null"
],
"markdownDescription": "Specify the output directory for generated declaration files.\n\nSee more: https://www.typescriptlang.org/tsconfig#declarationDir" "markdownDescription": "Specify the output directory for generated declaration files.\n\nSee more: https://www.typescriptlang.org/tsconfig#declarationDir"
}, },
"disableSizeLimit": { "disableSizeLimit": {
@@ -140,7 +145,14 @@
"jsx": { "jsx": {
"description": "Specify what JSX code is generated.", "description": "Specify what JSX code is generated.",
"type": "number", "type": "number",
"enum": [0, 1, 2, 3, 4, 5], "enum": [
0,
1,
2,
3,
4,
5
],
"markdownDescription": "Specify what JSX code is generated.\n\n0 = none\n\n1 = preserve\n\n2 = react\n\n3 = react-native\n\n4 = react-jsx\n\n5 = react-jsxdev" "markdownDescription": "Specify what JSX code is generated.\n\n0 = none\n\n1 = preserve\n\n2 = react\n\n3 = react-native\n\n4 = react-jsx\n\n5 = react-jsxdev"
}, },
"keyofStringsOnly": { "keyofStringsOnly": {
@@ -306,19 +318,33 @@
"module": { "module": {
"description": "Specify what module code is generated.", "description": "Specify what module code is generated.",
"type": "number", "type": "number",
"enum": [0, 1, 2, 3, 4, 5, 99], "enum": [
0,
1,
2,
3,
4,
5,
99
],
"markdownDescription": "Specify what module code is generated.\n\n0 = None\n\n1 = CommonJS\n\n2 = AMD\n\n3 = UMD\n\n4 = System\n\n5 = ES2015\n\n99 = ESNext" "markdownDescription": "Specify what module code is generated.\n\n0 = None\n\n1 = CommonJS\n\n2 = AMD\n\n3 = UMD\n\n4 = System\n\n5 = ES2015\n\n99 = ESNext"
}, },
"moduleResolution": { "moduleResolution": {
"description": "Specify how TypeScript looks up a file from a given module specifier.", "description": "Specify how TypeScript looks up a file from a given module specifier.",
"type": "number", "type": "number",
"enum": [1, 2], "enum": [
1,
2
],
"markdownDescription": "Specify how TypeScript looks up a file from a given module specifier.\n\n1 = Classic\n\n2 = NodeJs" "markdownDescription": "Specify how TypeScript looks up a file from a given module specifier.\n\n1 = Classic\n\n2 = NodeJs"
}, },
"newLine": { "newLine": {
"description": "Set the newline character for emitting files.", "description": "Set the newline character for emitting files.",
"type": "number", "type": "number",
"enum": [0, 1], "enum": [
0,
1
],
"markdownDescription": "Set the newline character for emitting files.\n\n0 = CarriageReturnLineFeed\n\n1 = LineFeed" "markdownDescription": "Set the newline character for emitting files.\n\n0 = CarriageReturnLineFeed\n\n1 = LineFeed"
}, },
"noEmit": { "noEmit": {
@@ -560,7 +586,19 @@
"description": "Set the JavaScript language version for emitted JavaScript and include compatible library declarations.", "description": "Set the JavaScript language version for emitted JavaScript and include compatible library declarations.",
"type": "number", "type": "number",
"default": 0, "default": 0,
"enum": [0, 1, 2, 3, 4, 5, 6, 7, 99, 100, 99], "enum": [
0,
1,
2,
3,
4,
5,
6,
7,
99,
100,
99
],
"markdownDescription": "Set the JavaScript language version for emitted JavaScript and include compatible library declarations.\n\n0 = ES3\n\n1 = ES5\n\n2 = ES2015\n\n3 = ES2016\n\n4 = ES2017\n\n5 = ES2018\n\n6 = ES2019\n\n7 = ES2020\n\n99 = ESNext\n\n100 = JSON\n\n99 = Latest" "markdownDescription": "Set the JavaScript language version for emitted JavaScript and include compatible library declarations.\n\n0 = ES3\n\n1 = ES5\n\n2 = ES2015\n\n3 = ES2016\n\n4 = ES2017\n\n5 = ES2018\n\n6 = ES2019\n\n7 = ES2020\n\n99 = ESNext\n\n100 = JSON\n\n99 = Latest"
}, },
"traceResolution": { "traceResolution": {
@@ -604,7 +642,6 @@
"default": false, "default": false,
"markdownDescription": "Emit ECMAScript-standard-compliant class fields.\n\nSee more: https://www.typescriptlang.org/tsconfig#useDefineForClassFields" "markdownDescription": "Emit ECMAScript-standard-compliant class fields.\n\nSee more: https://www.typescriptlang.org/tsconfig#useDefineForClassFields"
}, },
"allowArbitraryExtensions": { "allowArbitraryExtensions": {
"description": "Enable importing files with any extension, provided a declaration file is present.", "description": "Enable importing files with any extension, provided a declaration file is present.",
"type": "boolean", "type": "boolean",
@@ -773,12 +810,20 @@
}, },
"moduleDetection": { "moduleDetection": {
"description": "Specify how TypeScript determine a file as module.", "description": "Specify how TypeScript determine a file as module.",
"enum": ["auto", "legacy", "force"] "enum": [
"auto",
"legacy",
"force"
]
}, },
"importsNotUsedAsValues": { "importsNotUsedAsValues": {
"description": "Specify emit/checking behavior for imports that are only used for types.", "description": "Specify emit/checking behavior for imports that are only used for types.",
"default": "remove", "default": "remove",
"enum": ["remove", "preserve", "error"] "enum": [
"remove",
"preserve",
"error"
]
}, },
"resolvePackageJsonExports": { "resolvePackageJsonExports": {
"description": "Use the package.json 'exports' field when resolving package imports.", "description": "Use the package.json 'exports' field when resolving package imports.",
@@ -818,4 +863,5 @@
} }
} }
} }
}
} }

View File

@@ -169,18 +169,18 @@ const Base = () => {
)} )}
</> </>
), ),
width: '12em', width: '14em',
align: 'center', align: 'center',
render: (_, record) => ( render: (_, record) => (
<> <>
<AntdSpace size={'middle'}> <AntdSpace size={'middle'}>
{!record.compiled && !Object.keys(hasEdited).length && ( {!Object.keys(hasEdited).length && (
<Permission operationCode={['system:tool:modify:base']}> <Permission operationCode={['system:tool:modify:base']}>
<a <a
style={{ color: COLOR_PRODUCTION }} style={{ color: COLOR_PRODUCTION }}
onClick={handleOnCompileBtnClick(record)} onClick={handleOnCompileBtnClick(record)}
> >
{record.compiled ? '重新编译' : '编译'}
</a> </a>
</Permission> </Permission>
)} )}
@@ -304,7 +304,10 @@ const Base = () => {
![ ![
IMPORT_MAP_FILE_NAME, IMPORT_MAP_FILE_NAME,
TS_CONFIG_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 }))} .map((value) => ({ value, label: value }))}
placeholder={'请选择入口文件'} placeholder={'请选择入口文件'}
@@ -1097,7 +1100,7 @@ const Base = () => {
<Playground.CodeEditor <Playground.CodeEditor
files={editingFiles[editingBaseId]} files={editingFiles[editingBaseId]}
selectedFileName={editingFileName} selectedFileName={editingFileName}
onSelectedFileChange={() => {}} onSelectedFileChange={setEditingFileName}
onChangeFileContent={handleOnChangeFileContent} onChangeFileContent={handleOnChangeFileContent}
showFileSelector={false} showFileSelector={false}
tsconfig={tsconfig} tsconfig={tsconfig}

View File

@@ -1044,7 +1044,7 @@ const Template = () => {
<Playground.CodeEditor <Playground.CodeEditor
files={editingFiles[editingTemplateId]} files={editingFiles[editingTemplateId]}
selectedFileName={editingFileName} selectedFileName={editingFileName}
onSelectedFileChange={() => {}} onSelectedFileChange={setEditingFileName}
onChangeFileContent={handleOnChangeFileContent} onChangeFileContent={handleOnChangeFileContent}
showFileSelector={false} showFileSelector={false}
tsconfig={tsconfig} tsconfig={tsconfig}

View File

@@ -219,7 +219,7 @@ const Tools = () => {
<AntdForm.Item <AntdForm.Item
name={'pass'} name={'pass'}
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
rules={[{ required: true }]} rules={[{ required: true, message: '请选择审核结果' }]}
> >
<AntdRadio.Group> <AntdRadio.Group>
<AntdRadio value={true}></AntdRadio> <AntdRadio value={true}></AntdRadio>