[npm] esbuild-wasm #1

dev stefanCho·2021년 8월 29일
1

npm

목록 보기
1/8

esbuild 란?


브라우저 상에서 babel + webpack을 사용할 수 있게 해준다. 예를 들면, 마크다운 코드를 작성하고, 옆에 preview를 바로 보여주는 것이다.

  • ex) babeljs.io




특징


  • esbuild는 Go 언어로 만들어져 있다. (그래서 Transform API를 보면 Go 예시가 있다.)
  • js에서는 esbuild-wasm NPM module을 사용하면 된다. (wasm은 wrapper assembly의 약어이다. 즉, wrapper로 감싸져 있다고 생각하면 된다.)




react와 같은 npm을 compile하는 방법?


esbuild에서 esbuild.startService().transform()은 파일하나에 대해서 변환할 때 사용할 수 있다. esbuild.startService().build()는 여러 module이 import 되는 경우 사용한다. (version 0.9 이상부터는 esbuild.initialize()를 사용한다.)

error senario

  1. 사용자가 import react from 'react'을 입력한다.
  2. esbuild는 사용자의 hard drive에서 dependency를 찾으려고 한다.
  3. browser 내에서 esbuild를 run하기 때문에, file system(hard drive)에 access가 불가능하다.
  4. esbuild는 error를 throw 한다.

success senario

  1. 사용자가 import react from 'react'을 입력한다.
  2. esbuild는 사용자의 hard drive에서 dependency를 찾으려고 한다.
  3. esbuild에게 NPM registry에서 원하는 npm API의 데이터를 전달한다.




bundle


unpkg는 npm module을 cdn으로 사용할 수 있게 해주는 서비스이다.
npm registry api를 사용해도 되지만, 로컬환경에서는 CORS가 발생할 수 있으므로, unpkg를 사용해서 해볼 수 있다.




Example Code


App.tsx

버튼 클릭 시 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-path-plugins.ts

unpkg plugin에서 method

  • onResolve : path에 대해 처리한다. index.js를 최초에 처리한다.

    • A callback added using onResolve will be run on each import path in each module that esbuild builds.
  • onLoad : contents 내용을 loading한다.

    • A callback added using onLoad will be run for each unique path/namespace pair that has not been marked as external. Its job is to return the contents of the module and to tell esbuild how to interpret it.

처리방식

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에서는 위 코드를 리팩토링 합니다.

Ref

type esbuild.PluginBuild

profile
Front-end Developer

0개의 댓글