diff --git a/src/renderer/src/components/Playground/Output/Transform/index.tsx b/src/renderer/src/components/Playground/Output/Transform/index.tsx index d135018..6883751 100644 --- a/src/renderer/src/components/Playground/Output/Transform/index.tsx +++ b/src/renderer/src/components/Playground/Output/Transform/index.tsx @@ -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) diff --git a/src/renderer/src/components/Playground/compiler.ts b/src/renderer/src/components/Playground/compiler.ts index fb2db42..418d6ab 100644 --- a/src/renderer/src/components/Playground/compiler.ts +++ b/src/renderer/src/components/Playground/compiler.ts @@ -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((resolve) => { - if (this.init) { - resolve() - return - } - const timer = setInterval(() => { - if (this.init) { - clearInterval(timer) - resolve() + private waitInit = async () => { + if (!this.init) { + await new Promise((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((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(args.path) + build.onLoad({ filter: /.*/ }, async (args: OnLoadArgs): Promise => { + const cached = await this.fileCache.getItem(args.path) if (cached) { return cached } - const axiosResponse = await axios.get(args.path) - const result: esbuild.OnLoadResult = { - loader: 'js', - contents: axiosResponse.data, + const axiosResponse = await axios.get(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 => { + if (args.path === basePath) { + return { + loader: 'css', + contents: cssCode, + resolveDir: basePath + } + } + const cached = await this.fileCache.getItem(args.path) + + if (cached) { + return cached + } + + const axiosResponse = await axios.get(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 } diff --git a/src/renderer/src/components/Playground/files.ts b/src/renderer/src/components/Playground/files.ts index 4769b99..4577cbe 100644 --- a/src/renderer/src/components/Playground/files.ts +++ b/src/renderer/src/components/Playground/files.ts @@ -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}`