Open13

esm.shを理解したい

hashrockhashrock

結構Denoでフロントエンドやっててesm.shから先がわからないことになりがち

  • UIフレームワークとかを頑張ればimportできるようだけどesm.shの呪文みたいなのをみんな唱えててそれがよくわからない
  • esm.shはサーババージョンアップにともなってURLが指す内容が書き換わることがある。これはビルドが落ちたり型がエラーになったりdeno.lockのハッシュチェックに引っかかったりすることがある。pinという機能で避けられるはずなのだがよく理解できていない
  • npmモジュールの依存関係が結局どう解決されているのかわからない

例えばこれ。

https://github.com/denoland/fresh/discussions/606

hashrockhashrock

まずはesbuildを理解しよう。

こういうスクリプトを作る。

const leftPad = require('left-pad')
console.log(leftPad('foo', 5))

.\node_modules.bin\esbuild input.js --bundle --outfile=out.js

そうするとこうなる。

(() => {
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __commonJS = (cb, mod) => function __require() {
    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
  };

  // node_modules/left-pad/index.js
  var require_left_pad = __commonJS({
    "node_modules/left-pad/index.js"(exports, module) {
      "use strict";
      module.exports = leftPad2;
      var cache = [
        "",
        " ",
        "  ",
        "   ",
        "    ",
        "     ",
        "      ",
        "       ",
        "        ",
        "         "
      ];
      function leftPad2(str, len, ch) {
        str = str + "";
        len = len - str.length;
        if (len <= 0)
          return str;
        if (!ch && ch !== 0)
          ch = " ";
        ch = ch + "";
        if (ch === " " && len < 10)
          return cache[len] + str;
        var pad = "";
        while (true) {
          if (len & 1)
            pad += ch;
          len >>= 1;
          if (len)
            ch += ch;
          else
            break;
        }
        return pad + str;
      }
    }
  });

  // input.js
  var leftPad = require_left_pad();
  console.log(leftPad("foo", 5));
})();

hashrockhashrock

importならどうなる?

import leftPad from 'left-pad'
console.log(leftPad('foo', 5))
(() => {
  var __create = Object.create;
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getProtoOf = Object.getPrototypeOf;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __commonJS = (cb, mod) => function __require() {
    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
    // If the importer is in node compatibility mode or this is not an ESM
    // file that has been converted to a CommonJS file using a Babel-
    // compatible transform (i.e. "__esModule" has not been set), then set
    // "default" to the CommonJS "module.exports" for node compatibility.
    isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
    mod
  ));

  // node_modules/left-pad/index.js
  var require_left_pad = __commonJS({
    "node_modules/left-pad/index.js"(exports, module) {
      "use strict";
      module.exports = leftPad2;
      var cache = [
        "",
        " ",
        "  ",
        "   ",
        "    ",
        "     ",
        "      ",
        "       ",
        "        ",
        "         "
      ];
      function leftPad2(str, len, ch) {
        str = str + "";
        len = len - str.length;
        if (len <= 0)
          return str;
        if (!ch && ch !== 0)
          ch = " ";
        ch = ch + "";
        if (ch === " " && len < 10)
          return cache[len] + str;
        var pad = "";
        while (true) {
          if (len & 1)
            pad += ch;
          len >>= 1;
          if (len)
            ch += ch;
          else
            break;
        }
        return pad + str;
      }
    }
  });

  // input.js
  var import_left_pad = __toESM(require_left_pad());
  console.log((0, import_left_pad.default)("foo", 5));
})();

cjsとして処理されてるかな。

hashrockhashrock

じゃあ今度はesm.shからleft-padを使ってみよう。

import leftPad from 'https://esm.sh/left-pad'
console.log(leftPad('foo', 5))

これでどんなファイルがダウンロードされたかを見る。
Windowsだとここにキャッシュされる。

ユーザフォルダ\AppData\Local\deno\deps\https\esm.sh

// Type definitions for left-pad 1.2.0
// Project: https://github.com/stevemao/left-pad
// Definitions by: Zlatko Andonovski, Andrew Yang, Chandler Fang and Zac Xu

declare function leftPad(str: string|number, len: number, ch?: string|number): string;

declare namespace leftPad { }

export = leftPad;
/* esm.sh - left-pad@1.3.0 */
export * from "https://esm.sh/v103/left-pad@1.3.0/deno/left-pad.js";
export { default } from "https://esm.sh/v103/left-pad@1.3.0/deno/left-pad.js";
/* esm.sh - esbuild bundle(left-pad@1.3.0) deno production */
var _=Object.create;var a=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var n=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty;var v=(f,e)=>()=>(e||f((e={exports:{}}).exports,e),e.exports);var x=(f,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let u of m(e))!s.call(f,u)&&u!==t&&a(f,u,{get:()=>e[u],enumerable:!(r=p(e,u))||r.enumerable});return f};var b=(f,e,t)=>(t=f!=null?_(n(f)):{},x(e||!f||!f.__esModule?a(t,"default",{value:f,enumerable:!0}):t,f));var d=v((j,i)=>{"use strict";i.exports=k;var g=[""," ","  ","   ","    ","     ","      ","       ","        ","         "];function k(f,e,t){if(f=f+"",e=e-f.length,e<=0)return f;if(!t&&t!==0&&(t=" "),t=t+"",t===" "&&e<10)return g[e]+f;for(var r="";e&1&&(r+=t),e>>=1,e;)t+=t;return r+f}});var l=b(d()),{default:o,...w}=l,q=o!==void 0?o:w;export{q as default};

minifyされてる。devオプションをつけるとminifyが外れるようだ。

https://esm.sh/left-pad?dev

/* esm.sh - esbuild bundle(left-pad@1.3.0) es2022 development */
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

// esm-build-d2ddbf4b4c49a167a8953b3bb63c71b38ad24448-f9f2fc80/node_modules/left-pad/index.js
var require_left_pad = __commonJS({
  "esm-build-d2ddbf4b4c49a167a8953b3bb63c71b38ad24448-f9f2fc80/node_modules/left-pad/index.js"(exports, module) {
    "use strict";
    module.exports = leftPad;
    var cache = [
      "",
      " ",
      "  ",
      "   ",
      "    ",
      "     ",
      "      ",
      "       ",
      "        ",
      "         "
    ];
    function leftPad(str, len, ch) {
      str = str + "";
      len = len - str.length;
      if (len <= 0)
        return str;
      if (!ch && ch !== 0)
        ch = " ";
      ch = ch + "";
      if (ch === " " && len < 10)
        return cache[len] + str;
      var pad = "";
      while (true) {
        if (len & 1)
          pad += ch;
        len >>= 1;
        if (len)
          ch += ch;
        else
          break;
      }
      return pad + str;
    }
  }
});

// esm-build-d2ddbf4b4c49a167a8953b3bb63c71b38ad24448-f9f2fc80/mod.js
var __module = __toESM(require_left_pad());
var { default: __default, ...__rest } = __module;
var mod_default = __default !== void 0 ? __default : __rest;
export {
  mod_default as default
};
hashrockhashrock

内容的にはほとんどesbuildと同じ(当たり前)で、esm.sh側でもbundleモードで出力すればほとんど同じ出力内容になるのだろう。

hashrockhashrock

依存がある場合にはどうなってるのか。

依存が一つだけあるnpmパッケージとしてd3-arrayというのを見つけたのでこれでテストしてみよう。

https://github.com/d3/d3-array

hashrockhashrock

depsオプション

じゃあこれに対してdepsオプションを指定してみよう。古いd3-arrayを使いたいけどinternmapは新しいものを使いたい人がいることにする(辛そう)。

https://esm.sh/d3-array@2.12.1?dev&deps=internmap@2.0.3

するとURLになにかデータ入ってそうな感じになる。

https://esm.sh/v103/d3-array@2.12.1/X-ZC9pbnRlcm5tYXBAMi4wLjM/es2022/d3-array.development.js

ちゃんと内部でinternmap@2.0.3が使われている(動くかは確認してない)。

import { InternMap } from "/v103/internmap@2.0.3/es2022/internmap.development.js";

hashrockhashrock

aliasオプション

多分reactをpreactに差し替える用途で使ってるんだと思うけど、これを無理やりやってみる。

https://esm.sh/d3-array@2.12.1?dev&alias=internmap:d3-scale

これでinternmapというnpmモジュールをd3-scaleに差し替えられそう。hogehogeって名前指定しようかと思ったけどnpmに存在しないモジュールはエラーになるようだ(バージョン解決するために取りに行ってるのかな)

https://esm.sh/v103/d3-array@2.12.1/X-YS9pbnRlcm5tYXA6ZDMtc2NhbGU/es2022/d3-array.development.js

するとこうなった。

import { InternMap } from "/v103/d3-scale@4.0.2/X-YS9pbnRlcm5tYXA6ZDMtc2NhbGU/es2022/d3-scale.development.js";

当然こんなの動かないと思うけど、差し替わってる。完全にインターフェース揃えられるなら便利そう。

hashrockhashrock

externalオプション

これは多分普通に解決せずに残すだけなんだろうなあ。esbuildに同じ項目があるからそのまんまなんだと思う。

https://esm.sh/d3-array@3.2.2?dev&external=internmap

https://esm.sh/v103/d3-array@3.2.2/X-ZS9pbnRlcm5tYXA/es2022/d3-array.development.js

想像通りの結果になった。

import { InternMap } from "internmap";

公式ドキュメントでも言及されている通り、これをdeno側で解決するにはimport mapの設定が必要になる。基本的に差し替え技法の一つという感じ。

このアスタリスクのやつも見ておこう。

https://esm.sh/*d3-array@3.2.2?dev

やはりこうなった。

import { InternMap } from "internmap";

hashrockhashrock

pin

esm.sh自体のバージョンを固定してコンテンツを返すことができるらしい。直感的には大変そうだけど、リクエストがあったものは保持しておくみたいな仕組みなのかな?

とりあえずローカルで比較してみたが、下記の2つには差はない。

https://esm.sh/d3-array@3.2.2?pin=v50
https://esm.sh/d3-array@3.2.2?pin=v103

ちなみにこの形式のURLでも起きることはいっしょ。

https://esm.sh/v103/d3-array@3.2.2

なるほど~…
じゃあよく使われるpreactとかreactで試してみよう。

https://esm.sh/v103/preact@10.6.6

あれ、これは…

export * from "https://esm.sh/stable/preact@10.6.6/es2022/preact.js";

バージョンが消えてstableというURLになってしまう。pin指定しても全部目先が同じ。

これはなんだろう。よく使われるパッケージは特別扱いでpinも無視してstable版を配信してるのかな?確かに書き換わると大騒ぎになりそうな部分ではあるけど…

twindとかでも試してみたけどpinによる差分は確認できず。うーんいい実験条件が思いつかないな。

hashrockhashrock

今のところサーババージョンによる大きな差分は見当たらないので、esm.shから取得したモジュールがdeno.lockで死にがちなのは、下記の動的なコメントが原因になってるのがほとんどなんじゃないかな。

// esm-build-cbc9d8f829f1aff3f714aa811466c0596c5e3f42-ddc0fc8b/node_modules/d3-array/src/ascending.js

もうちょい知らないケースがありそうだけど、ひとまずざっくりオプション周りはわかった気がする。