Refactor(compiler): Support load css from ImportMap and load binary file
This commit is contained in:
@@ -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 } 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'
|
||||||
|
|
||||||
@@ -39,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)
|
||||||
|
|||||||
@@ -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 localforage from 'localforage'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'
|
import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'
|
||||||
import { IFile, 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
|
||||||
@@ -27,44 +41,47 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transform = (code: string, loader: Loader) =>
|
private waitInit = async () => {
|
||||||
new Promise<void>((resolve) => {
|
if (!this.init) {
|
||||||
if (this.init) {
|
await new Promise<void>((resolve) => {
|
||||||
resolve()
|
const checkInit = () => {
|
||||||
return
|
if (this.init) {
|
||||||
}
|
resolve()
|
||||||
const timer = setInterval(() => {
|
} else {
|
||||||
if (this.init) {
|
setTimeout(checkInit, 100)
|
||||||
clearInterval(timer)
|
}
|
||||||
resolve()
|
|
||||||
}
|
}
|
||||||
}, 100)
|
checkInit()
|
||||||
}).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)]
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
stop = () => {
|
||||||
void esbuild.stop()
|
void esbuild.stop()
|
||||||
@@ -73,7 +90,7 @@ class Compiler {
|
|||||||
private fileResolverPlugin = (files: IFiles, importMap: IImportMap): Plugin => ({
|
private fileResolverPlugin = (files: IFiles, importMap: IImportMap): 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 (args.kind === 'entry-point') {
|
if (args.kind === 'entry-point') {
|
||||||
return {
|
return {
|
||||||
namespace: 'oxygen',
|
namespace: 'oxygen',
|
||||||
@@ -135,54 +152,152 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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({ namespace: 'oxygen', filter: /.*/ }, (args: OnLoadArgs) => {
|
build.onLoad(
|
||||||
let file: IFile | undefined
|
{ namespace: 'oxygen', filter: /.*/ },
|
||||||
|
(args: OnLoadArgs): OnLoadResult | undefined => {
|
||||||
|
let file: IFile | undefined
|
||||||
|
|
||||||
void ['', '.tsx', '.jsx', '.ts', '.js'].forEach((suffix) => {
|
void ['', '.tsx', '.jsx', '.ts', '.js'].forEach((suffix) => {
|
||||||
file = file || files[`${args.path}${suffix}`]
|
file = file || files[`${args.path}${suffix}`]
|
||||||
})
|
})
|
||||||
if (file) {
|
if (file) {
|
||||||
return {
|
return {
|
||||||
loader: (() => {
|
loader: (() => {
|
||||||
switch (file.language) {
|
switch (file.language) {
|
||||||
case 'javascript':
|
case 'javascript':
|
||||||
return 'jsx'
|
return 'jsx'
|
||||||
default:
|
default:
|
||||||
return 'tsx'
|
return 'tsx'
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
contents: addReactImport(file.value)
|
contents: addReactImport(file.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs) => {
|
build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs): Promise<OnLoadResult> => {
|
||||||
const cached = await this.fileCache.getItem<esbuild.OnLoadResult>(args.path)
|
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: 'js',
|
})
|
||||||
contents: axiosResponse.data,
|
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
|
resolveDir: args.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +109,10 @@ 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 (!/^\s*import\s+React\s+/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}`
|
||||||
|
|||||||
Reference in New Issue
Block a user