브라우저 상에서 babel + webpack을 사용할 수 있게 해준다. 예를 들면, 마크다운 코드를 작성하고, 옆에 preview를 바로 보여주는 것이다.
esbuild에서 esbuild.startService().transform()은 파일하나에 대해서 변환할 때 사용할 수 있다. esbuild.startService().build()는 여러 module이 import 되는 경우 사용한다. (version 0.9 이상부터는 esbuild.initialize()를 사용한다.)
import react from 'react'
을 입력한다.import react from 'react'
을 입력한다.unpkg는 npm module을 cdn으로 사용할 수 있게 해주는 서비스이다.
npm registry api를 사용해도 되지만, 로컬환경에서는 CORS가 발생할 수 있으므로, unpkg를 사용해서 해볼 수 있다.
버튼 클릭 시 esbuild로 input을 변환하여, code variable에 넣는다.
wasmURL에는 파일을 직접 넣어서 (node-modules에서 esbuild-wasm의 esbuild.wasm 파일을 public경로 아래에 두면 된다.) 경로 지정을 할 수도 있고 (아래코드 처럼), 혹은 wasmURL: "https://unpkg.com/esbuild-wasm@0.8.27/esbuild.wasm"
로 하면, unpkg.com에서 직접 다운로드 받게 할 수도 있다.
// App.tsx
import { useState, useEffect, useRef } from "react";
import * as esbuild from 'esbuild-wasm';
import { unpkgPathPlugin } from "../plugins/unpkg-path-plugin";
const App = () => {
const ref = useRef<any>();
const [input, setInput] = useState('');
const [code, setCode] = useState('');
const startService = async () => {
ref.current = await esbuild.startService({
worker: true,
wasmURL: "/esbuild.wasm", // url is public/esbuild.wasm
});
};
useEffect(() => {
startService();
}, [])
const onClick = async () => {
if (!ref.current) {
return;
}
const result = await ref.current.build({
entryPoints: ['index.js'],
bundle: true,
write: false,
plugins: [unpkgPathPlugin()], // entry point를 intercept한다. (아래 unpkg-path-plugins.ts 코드에서 설명)
define: {
'process.env.NODE_ENV': '"production"',
global: 'window', // replace global to window
}
});
// const result = await ref.current.transform(input, {
// loader: 'jsx',
// target: ['es2015'],
// });
// setCode(result.code);
setCode(result.outputFiles[0].text);
}
return (
<div>
<textarea value={input} onChange={(e) => setInput(e.target.value)} />
<div>
<button onClick={onClick}>Submit</button>
</div>
<pre>{code}</pre>
</div>
);
}
export default App;
unpkg plugin에서 method
onResolve : path에 대해 처리한다. index.js를 최초에 처리한다.
onLoad : contents 내용을 loading한다.
처리방식
import/export가 코드에서 확인 되면, onResolve --> onLoad 순서를 반복한다. (onResolve의 namespace가 'a', onLoad의 namespace가 'b'이라면 -> onLoad 단계에서 namespace가 'b'인 onResolve를 찾지 못하기 때문에, error가 발생한다.)
unpkg-path-plugins.ts가 entryPoints를 intercept한다.
App.tsx에서 esbuild.build()의 entryPoints를 index.js로 했다. unpkg-path-plugins.ts은 index.js를 intercept하기 위한 파일이다.
- App.tsx : entryPoints가 index.js
- unpkg-path-plugins.ts : index.js 대신에 내가 처리해 줄게
// App.tsx의 코드 일부분 -----------------------------
const result = await ref.current.build({
entryPoints: ['index.js'],
bundle: true,
write: false,
plugins: [unpkgPathPlugin()], // entry point를 intercept한다. (아래 unpkg-path-plugins.ts 코드에서 설명)
define: {
'process.env.NODE_ENV': '"production"',
global: 'window', // replace global to window
}
});
// ------------------------------------------------
// unpkg-path-plugins.ts
import * as esbuild from 'esbuild-wasm';
import axios from 'axios';
export const unpkgPathPlugin = () => {
return {
name: 'unpkg-path-plugin',
setup(build: esbuild.PluginBuild) {
// try to figure out where the index.js file is stored
build.onResolve({ filter: /.*/ }, async (args: any) => {
console.log("onResolve", args);
if (args.path === "index.js") { // index.js라면 아래를 시도하라
return { path: args.path, namespace: "a" };
}
if (args.path.includes("./") || args.path.includes("../")) { // for relatvie path
return {
path: new URL(args.path, 'https://unpkg.com' + args.resolveDir + '/').href,
namespace: "a",
};
}
return {
path: `https://unpkg.com/${args.path}`,
namespace: 'a',
}
});
// attempt to load up the index.js file
build.onLoad({ filter: /.*/ }, async (args: any) => {
console.log('onLoad', args);
if (args.path === 'index.js') { // index.js라면 아래를 시도하라
return {
loader: 'jsx',
contents: `
const React = require('react');
console.log(React); // 임의로 예시 코드를 넣어봤다.
`,
};
}
const { data, request } = await axios.get(args.path);
return {
loader: 'jsx',
contents: data,
resolveDir: new URL('./', request.responseURL).pathname, // nested import를 처리하기 위해서 resolveDir를 사용한다. (예를 들면 src/index.js에서 src/components/test.js를 import한다면, src/ 라는 path를 유지해야한다.)
}
});
},
};
};
다음 블로그 #2에서는 위 코드를 리팩토링 합니다.