feat: 新增本地文件编译
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||

|
||||
|
||||
一个运行在浏览器中`esbuild-wasm`的文件解析器
|
||||
一个运行在浏览器中打包编译器,基于`esbuild-wasm`
|
||||
|
||||
## 介绍
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
使`esbuild-wasm`可以解析来自IndexedDB、LocalStorage、Http或任何其他浏览器可访问的可读设备的文件。
|
||||
|
||||
`esbuild-wasm-compiler`主要提供给编辑器使用,或者在浏览器中编译执行项目代码。
|
||||
`esbuild-wasm-compiler`主要提供给编辑器使用,或者示例演示,或者在浏览器中编译执行项目代码。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -21,16 +21,12 @@ $ npm install @rainetian/esbuild-wasm-compiler
|
||||
|
||||
[//]: # (在浏览器中执行react项目)
|
||||
|
||||
## 使用
|
||||
#### 从js对象中读取文件
|
||||
|
||||
### html中使用
|
||||
|
||||
[example](https://github.com/fewismuch/esbuild-wasm-compiler/blob/main/example/index.html)
|
||||
|
||||
### 项目中使用
|
||||
[example](https://github.com/fewismuch/esbuild-wasm-compiler/blob/main/example/demo1/index.html)
|
||||
|
||||
```javascript
|
||||
import {Compiler,kvFilesResolver} from '@rainetian/esbuild-wasm-compiler'
|
||||
import {Compiler} from '@rainetian/esbuild-wasm-compiler'
|
||||
import {files} from './files'
|
||||
|
||||
const compiler = new Compiler({
|
||||
@@ -52,77 +48,39 @@ console.log(code);
|
||||
|
||||
```
|
||||
|
||||
`./files`
|
||||
#### 从文件系统中读取文件
|
||||
|
||||
[example2](https://github.com/fewismuch/esbuild-wasm-compiler/blob/main/example/demo2/index.html)
|
||||
|
||||
|
||||
```javascript
|
||||
const AppCode = `
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {Compiler} from '@rainetian/esbuild-wasm-compiler'
|
||||
|
||||
const App = () => {
|
||||
const [num, setNum] = React.useState<number>(1)
|
||||
return <>
|
||||
<button onClick={() => setNum(num + 1)}>click</button>
|
||||
<span>{num}</span>
|
||||
</>
|
||||
}
|
||||
ReactDOM.render(<App/>, document.getElementById("root"));
|
||||
`;
|
||||
|
||||
const packageJson = `
|
||||
{
|
||||
"name": "react-ts",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const files = {
|
||||
"/App.tsx": AppCode,
|
||||
"package.json":packageJson
|
||||
};
|
||||
Compiler.createApp('./main.tsx').mount('#root')
|
||||
```
|
||||
|
||||
|
||||
## 配置项
|
||||
|
||||
```typescript
|
||||
export interface FilesResolver {
|
||||
getFileContent(path: string): Promise<string> | string;
|
||||
}
|
||||
|
||||
export interface CompilerOptions extends esbuild.InitializeOptions {
|
||||
/**
|
||||
* package.json文件内容
|
||||
*/
|
||||
packageJson?: Record<string, any>;
|
||||
/**
|
||||
* 是否替换第三方依赖包包名为importMap中的url,默认true
|
||||
*/
|
||||
replaceImports?: boolean;
|
||||
}
|
||||
|
||||
export declare class Compiler {
|
||||
constructor(resolver: FilesResolver, options?: CompilerOptions | undefined);
|
||||
compile(entryPoint: string, options?: esbuild.BuildOptions): Promise<string>;
|
||||
/**
|
||||
* 获取importmap script标签
|
||||
*/
|
||||
getImportsScriptElement(): HTMLScriptElement;
|
||||
compile(entryPoint: string, options?: esbuild.BuildOptions): Promise<string | {
|
||||
error: boolean;
|
||||
message: string;
|
||||
}>;
|
||||
static createApp(path: string): Compiler;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>esbuild-wasm-demo</title>
|
||||
<!--<script src="https://cdn.jsdelivr.net/npm/@rainetian/esbuild-wasm-compiler/dist/esbuild-wasm-compiler.min.js"></script>-->
|
||||
<script src="../../dist/esbuild-wasm-compiler.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">loading...</div>
|
||||
|
||||
<script>
|
||||
let Main = `
|
||||
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>
|
||||
)
|
||||
`
|
||||
let AppCode = `
|
||||
import React from 'react'
|
||||
import {Button} from './Button'
|
||||
import './index.css'
|
||||
|
||||
const App = ()=>{
|
||||
const [count,setCount]=React.useState<number>(1)
|
||||
return <>
|
||||
<button onClick={()=>setCount(count+1)}>click</button>
|
||||
<div className='my-count'>count:{count}</div>
|
||||
<Button/>
|
||||
</>
|
||||
}
|
||||
export default App
|
||||
`
|
||||
|
||||
const ButtonCode = `
|
||||
import React from 'react'
|
||||
import {random} from 'lodash-es'
|
||||
|
||||
export const Button = ()=>{
|
||||
return <button>this is a btn comp, random: {random(1,100)}</button>
|
||||
}`
|
||||
|
||||
const indexCssCode = `.my-count{color:red}`
|
||||
|
||||
const files = {
|
||||
'/main.tsx': Main,
|
||||
'/App.tsx': AppCode,
|
||||
'/Button.tsx': ButtonCode,
|
||||
'/index.css': indexCssCode,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const compiler = new Compiler(
|
||||
{
|
||||
getFileContent: (path) => {
|
||||
const filePath = Object.keys(files).find((item) => item.startsWith(path))
|
||||
const content = filePath ? files[filePath] : null
|
||||
if (!content) {
|
||||
throw new Error('File not found')
|
||||
}
|
||||
return content
|
||||
},
|
||||
},
|
||||
{
|
||||
wasmURL: '../esbuild.wasm',
|
||||
}
|
||||
)
|
||||
|
||||
const init = async () => {
|
||||
const code = await compiler.compile('/main.tsx')
|
||||
console.log(code)
|
||||
// 编译报错信息
|
||||
if (typeof code !== 'string' && code.error) {
|
||||
document.querySelector('#root').innerHTML = code.message
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.type = 'module'
|
||||
script.innerHTML = code
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
export const App: FC<{ name: string }> = ({ name }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {name}!</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>esbuild-wasm-demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">loading...</div>
|
||||
<!--<script src="https://cdn.jsdelivr.net/npm/@rainetian/esbuild-wasm-compiler/dist/esbuild-wasm-compiler.min.js"></script>-->
|
||||
<script src="../../dist/esbuild-wasm-compiler.min.js"></script>
|
||||
<script>
|
||||
<!-- Compiler.createApp静态方法内置了读取当前文件夹文件的FilesResolver,简化使用,也可同demo1一样自定义 -->
|
||||
Compiler.createApp('./main.tsx').mount('#root')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { App } from './App.tsx'
|
||||
|
||||
const root = createRoot(document.getElementById('root'))
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App name='rainetian' />
|
||||
</StrictMode>
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>esbuild-wasm-demo</title>
|
||||
<!--<script src="https://cdn.jsdelivr.net/npm/@rainetian/esbuild-wasm-compiler/dist/esbuild-wasm-compiler.min.js"></script>-->
|
||||
<script src="../dist/esbuild-wasm-compiler.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
loading...
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let Main = `
|
||||
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>
|
||||
)
|
||||
`
|
||||
let AppCode = `
|
||||
import React from 'react'
|
||||
import {Button} from './Button'
|
||||
import './index.css'
|
||||
|
||||
const App = ()=>{
|
||||
const [count,setCount]=React.useState<number>(1)
|
||||
return <>
|
||||
<button onClick={()=>setCount(count+1)}>click</button>
|
||||
<div className='my-count'>count:{count}</div>
|
||||
<Button/>
|
||||
</>
|
||||
}
|
||||
export default App
|
||||
`;
|
||||
|
||||
const ButtonCode = `
|
||||
import React from 'react'
|
||||
import {random} from 'lodash-es'
|
||||
|
||||
export const Button = ()=>{
|
||||
return <button>this is a btn comp, random: {random(1,100)}</button>
|
||||
}`;
|
||||
|
||||
const indexCssCode = `.my-count{color:red}`;
|
||||
|
||||
const files = {
|
||||
"/main.tsx": Main,
|
||||
"/App.tsx": AppCode,
|
||||
"/Button.tsx": ButtonCode,
|
||||
"/index.css": indexCssCode,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const compiler = new Compiler({
|
||||
getFileContent: path => {
|
||||
const filePath = Object.keys(files).find(item => item.startsWith(path))
|
||||
const content = filePath ? files[filePath] : null
|
||||
if (!content) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
})
|
||||
|
||||
const init = async () => {
|
||||
const code = await compiler.compile('/main.tsx')
|
||||
console.log(code)
|
||||
// 编译报错信息
|
||||
if (typeof code !== 'string' && code.error) {
|
||||
document.querySelector('#root').innerHTML = code.message
|
||||
return
|
||||
}
|
||||
const script = document.createElement("script");
|
||||
script.type = "module";
|
||||
script.innerHTML = code
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rainetian/esbuild-wasm-compiler",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.10",
|
||||
"private": false,
|
||||
"description": "File Resolution for Esbuild running in the Browser",
|
||||
"keywords": [
|
||||
|
||||
+83
-45
@@ -1,6 +1,6 @@
|
||||
import * as esbuild from 'esbuild-wasm'
|
||||
import { Path } from '../path'
|
||||
import { css2Js, getEsmUrl, getLoaderByLang, omit } from './utils'
|
||||
import { beforeTransformCodeHandler, css2Js, getEsmUrl, getLoaderByLang, omit } from './utils'
|
||||
|
||||
export interface FilesResolver {
|
||||
getFileContent(path: string): Promise<string> | string
|
||||
@@ -27,6 +27,7 @@ const DEFAULT_COMPILER_OPTIONS: CompilerOptions = {
|
||||
export class Compiler {
|
||||
private readonly decoder: TextDecoder
|
||||
private initialized: boolean = false
|
||||
private mount: ((selector: string) => Promise<void>) | undefined
|
||||
|
||||
constructor(
|
||||
private readonly resolver: FilesResolver,
|
||||
@@ -43,50 +44,6 @@ export class Compiler {
|
||||
})
|
||||
}
|
||||
|
||||
public async compile(entryPoint: string, options: esbuild.BuildOptions = {}) {
|
||||
while (!this.initialized) {
|
||||
// Wait until initialization is complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
}
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await esbuild.build({
|
||||
entryPoints: [entryPoint.charAt(0) === '/' ? entryPoint.slice(1) : entryPoint],
|
||||
plugins: [
|
||||
{
|
||||
name: 'browserResolve',
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /.*/ }, async (args) => this.onResolveCallback(args))
|
||||
build.onLoad({ filter: /.*/ }, (args) => this.onLoadCallback(args))
|
||||
},
|
||||
},
|
||||
...(options.plugins || []),
|
||||
],
|
||||
sourcemap: 'inline',
|
||||
target: 'es2015',
|
||||
platform: 'browser',
|
||||
format: 'esm',
|
||||
...omit(options, ['plugins']),
|
||||
// required
|
||||
bundle: true,
|
||||
write: false,
|
||||
})
|
||||
const contents = result.outputFiles![0].contents
|
||||
return this.decoder.decode(contents)
|
||||
} catch (e: any) {
|
||||
let formatted = await esbuild.formatMessages(e.errors, {
|
||||
kind: 'error',
|
||||
color: false,
|
||||
terminalWidth: 100,
|
||||
})
|
||||
return {
|
||||
error: true,
|
||||
message: formatted.join('\n'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async onResolveCallback(args: esbuild.OnResolveArgs) {
|
||||
if (args.kind === 'entry-point') {
|
||||
return { path: '/' + args.path }
|
||||
@@ -140,6 +97,87 @@ export class Compiler {
|
||||
const name = args.path
|
||||
contents = await css2Js(name, contents)
|
||||
}
|
||||
if (['.jsx', '.tsx']) {
|
||||
contents = beforeTransformCodeHandler(contents)
|
||||
}
|
||||
return { contents, loader }
|
||||
}
|
||||
|
||||
public async compile(entryPoint: string, options: esbuild.BuildOptions = {}) {
|
||||
while (!this.initialized) {
|
||||
// Wait until initialization is complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
}
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await esbuild.build({
|
||||
entryPoints: [entryPoint.charAt(0) === '/' ? entryPoint.slice(1) : entryPoint],
|
||||
plugins: [
|
||||
{
|
||||
name: 'browserResolve',
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /.*/ }, async (args) => this.onResolveCallback(args))
|
||||
build.onLoad({ filter: /.*/ }, (args) => this.onLoadCallback(args))
|
||||
},
|
||||
},
|
||||
...(options.plugins || []),
|
||||
],
|
||||
sourcemap: 'inline',
|
||||
target: 'es2015',
|
||||
platform: 'browser',
|
||||
format: 'esm',
|
||||
...omit(options, ['plugins']),
|
||||
// required
|
||||
bundle: true,
|
||||
write: false,
|
||||
})
|
||||
const contents = result.outputFiles![0].contents
|
||||
return this.decoder.decode(contents)
|
||||
} catch (e: any) {
|
||||
let formatted = await esbuild.formatMessages(e.errors, {
|
||||
kind: 'error',
|
||||
color: false,
|
||||
terminalWidth: 100,
|
||||
})
|
||||
return {
|
||||
error: true,
|
||||
message: formatted.join('\n'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compiler.createApp('./main.tsx').mount('#root')
|
||||
public static createApp(path: string) {
|
||||
const compiler = new Compiler({
|
||||
getFileContent: async (path) => {
|
||||
const content = await fetch(`.${path}`).then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error('File not found')
|
||||
}
|
||||
return res.text()
|
||||
})
|
||||
return content
|
||||
},
|
||||
})
|
||||
|
||||
compiler.mount = async (selector: string) => {
|
||||
const root = document.querySelector(selector)
|
||||
if (!root) {
|
||||
throw new Error('Root element not found')
|
||||
}
|
||||
const code = await compiler.compile(path)
|
||||
if (typeof code !== 'string' && code.error) {
|
||||
root.innerHTML = code.message
|
||||
return
|
||||
}
|
||||
if (typeof code === 'string') {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'module'
|
||||
script.innerHTML = code
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
}
|
||||
return compiler
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,3 +98,13 @@ export const getEsmUrl = (dependencies: Record<string, string> | null, path: str
|
||||
return `https://esm.sh/${esmName}`
|
||||
}
|
||||
}
|
||||
|
||||
export const beforeTransformCodeHandler = (code: string) => {
|
||||
let _code = code
|
||||
// 如果没有引入React,开头添加React引用
|
||||
const regexReact = /import\s+React/g
|
||||
if (!regexReact.test(code)) {
|
||||
_code = `import React from 'react';\n${code}`
|
||||
}
|
||||
return _code
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user