feat: 新增本地文件编译

This commit is contained in:
tianyu
2024-03-03 17:27:00 +08:00
parent a1c5427276
commit 7b041daa3d
9 changed files with 241 additions and 195 deletions
+18 -60
View File
@@ -2,7 +2,7 @@
![NPM version](https://img.shields.io/npm/v/@rainetian/esbuild-wasm-compiler.svg?style=flat)
一个运行在浏览器中`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;
}
```
## 参考
+92
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
import { FC } from 'react'
export const App: FC<{ name: string }> = ({ name }) => {
return (
<div>
<h1>Hello {name}!</h1>
</div>
)
}
+16
View File
@@ -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>
+12
View File
@@ -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>
)
-89
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+10
View File
@@ -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
}