Refactor(compiler): Support load css from ImportMap and load binary file

This commit is contained in:
2024-10-26 16:55:16 +08:00
parent f3a6f87dff
commit 757c27823a
3 changed files with 197 additions and 74 deletions

View File

@@ -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 } 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'
@@ -39,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)

View File

@@ -1,9 +1,23 @@
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 esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'
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 {
private init = false
@@ -27,44 +41,47 @@ 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)]
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()
@@ -73,7 +90,7 @@ class Compiler {
private fileResolverPlugin = (files: IFiles, importMap: IImportMap): Plugin => ({
name: 'file-resolver-plugin',
setup: (build: PluginBuild) => {
build.onResolve({ filter: /.*/ }, (args: esbuild.OnResolveArgs) => {
build.onResolve({ filter: /.*/ }, (args: OnResolveArgs): OnResolveResult => {
if (args.kind === 'entry-point') {
return {
namespace: 'oxygen',
@@ -135,54 +152,152 @@ class Compiler {
}
})
build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs) => {
const contents = cssToJs(files[args.path])
build.onLoad({ filter: /.*\.css$/ }, (args: OnLoadArgs): OnLoadResult => {
const contents = cssToJsFromFile(files[args.path])
return {
loader: 'js',
contents
}
})
build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs) => {
const contents = jsonToJs(files[args.path])
build.onLoad({ filter: /.*\.json$/ }, (args: OnLoadArgs): OnLoadResult => {
const contents = jsonToJsFromFile(files[args.path])
return {
loader: 'js',
contents
}
})
build.onLoad({ namespace: 'oxygen', filter: /.*/ }, (args: OnLoadArgs) => {
let file: IFile | undefined
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)
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) => {
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) {
return cached
}
const axiosResponse = await axios.get<string>(args.path)
const result: esbuild.OnLoadResult = {
loader: 'js',
contents: axiosResponse.data,
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
}

View File

@@ -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,6 +109,10 @@ export const cssToJs = (file: IFile) => {
`
}
export const cssToJsFromFile = (file: IFile) => {
return cssToJs(file.value, file.name)
}
export const addReactImport = (code: string) => {
if (!/^\s*import\s+React\s+/g.test(code)) {
return `import React from 'react';\n${code}`