esm.shを理解したい
結構Denoでフロントエンドやっててesm.shから先がわからないことになりがち
- UIフレームワークとかを頑張ればimportできるようだけどesm.shの呪文みたいなのをみんな唱えててそれがよくわからない
- esm.shはサーババージョンアップにともなってURLが指す内容が書き換わることがある。これはビルドが落ちたり型がエラーになったりdeno.lockのハッシュチェックに引っかかったりすることがある。pinという機能で避けられるはずなのだがよく理解できていない
- npmモジュールの依存関係が結局どう解決されているのかわからない
例えばこれ。
まずは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));
})();
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として処理されてるかな。
じゃあ今度は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が外れるようだ。
/* 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
};
内容的にはほとんどesbuildと同じ(当たり前)で、esm.sh側でもbundleモードで出力すればほとんど同じ出力内容になるのだろう。
依存がある場合にはどうなってるのか。
依存が一つだけあるnpmパッケージとしてd3-arrayというのを見つけたのでこれでテストしてみよう。
バージョン未指定の場合は
最新版にリダイレクトされる。
ソースコード本体はここ。
https://esm.sh/v103/d3-array@3.2.2/es2022/d3-array.development.js
ここからInternMapというクラスを使っている。
import { InternMap } from "/v103/internmap@2.0.3/es2022/internmap.development.js";
最後はここ。
https://esm.sh/v103/internmap@2.0.3/es2022/internmap.development.js
古いinternmapを使っていたバージョン(2系の最後)と比較してみよう。
https://esm.sh/v103/d3-array@2.12.1/es2022/d3-array.development.js
普通に古いバージョンにリンクされる(当たり前体操)。
import { InternMap } from "/v103/internmap@1.0.1/es2022/internmap.development.js";
depsオプション
じゃあこれに対してdepsオプションを指定してみよう。古いd3-arrayを使いたいけどinternmapは新しいものを使いたい人がいることにする(辛そう)。
すると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";
aliasオプション
多分reactをpreactに差し替える用途で使ってるんだと思うけど、これを無理やりやってみる。
これで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";
当然こんなの動かないと思うけど、差し替わってる。完全にインターフェース揃えられるなら便利そう。
externalオプション
これは多分普通に解決せずに残すだけなんだろうなあ。esbuildに同じ項目があるからそのまんまなんだと思う。
https://esm.sh/v103/d3-array@3.2.2/X-ZS9pbnRlcm5tYXA/es2022/d3-array.development.js
想像通りの結果になった。
import { InternMap } from "internmap";
公式ドキュメントでも言及されている通り、これをdeno側で解決するにはimport mapの設定が必要になる。基本的に差し替え技法の一つという感じ。
このアスタリスクのやつも見ておこう。
やはりこうなった。
import { InternMap } from "internmap";
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でも起きることはいっしょ。
なるほど~…
じゃあよく使われるpreactとかreactで試してみよう。
あれ、これは…
export * from "https://esm.sh/stable/preact@10.6.6/es2022/preact.js";
バージョンが消えてstableというURLになってしまう。pin指定しても全部目先が同じ。
これはなんだろう。よく使われるパッケージは特別扱いでpinも無視してstable版を配信してるのかな?確かに書き換わると大騒ぎになりそうな部分ではあるけど…
twindとかでも試してみたけどpinによる差分は確認できず。うーんいい実験条件が思いつかないな。
今のところサーババージョンによる大きな差分は見当たらないので、esm.shから取得したモジュールがdeno.lockで死にがちなのは、下記の動的なコメントが原因になってるのがほとんどなんじゃないかな。
// esm-build-cbc9d8f829f1aff3f714aa811466c0596c5e3f42-ddc0fc8b/node_modules/d3-array/src/ascending.js
もうちょい知らないケースがありそうだけど、ひとまずざっくりオプション周りはわかった気がする。