CDN からの import が自動解決されたらいいのにな、っていう妄想

この前思った夢というか妄想を書いておく。


例えば「Angular 製の SPA に、SASS 版の Bootstrap を組み込んでデザインしてアプリ作りましたー」っていう時に、アプリをビルドすると、

…といった JS・CSS ファイルが生成される。各ライブラリやビルドツールの方で Tree-Shaking などのチューニングを頑張っているだろうが、ちょっとしたフロントエンドアプリを作ると、ビルド後のファイルサイズは 1MB を軽く超えてくるのも不思議ではないと思う。

しかし、そんなアプリの src/ ディレクトリのサイズを見てみると、1MB もなくて 2・300KB 程度だったりする。自分が実際に書いたコードの量なんて大抵はしれていて、フレームワークとか依存ライブラリとか、アプリ内で拝借している「先人の知恵」の方がファイルサイズを食っていることはザラだと思う。

全てをバンドルしてビルドすれば、そのサイトからダウンロードしたアセットだけでアプリ全体が間違いなく動作するので、安定性でいえば確実な方法である。


一方、最近は ESModules の流れもあって、CDN 経由でライブラリを読み込んで利用する場面も増えてきたと思う。以下の Gist にあるコードなんかがとても分かりやすい。

<script type="module" src="https://jspm.dev/@babel/standalone"></script>
<script type="text/jsx" data-type="module">
  import React from "https://jspm.dev/react";
  import { createRoot } from "https://jspm.dev/react-dom";
  // ↑ ここまでが依存パッケージのインポート
  
  // ↓ ここから下が開発者が実装したアプリコード
  const root = createRoot(document.querySelector("#main"));
  root.render(<div>Hello, world!</div>);
</script>
<div id="main"></div>

ちょっとだけコードを調整させてもらったが、コレだけで React アプリとして動作している。

React 本体と、それを動かすために Babel を使用しているが、いずれも jspm.dev という CDN サイトからライブラリのコードをダウンロードしていて、最小限の記述でアプリが構築できている。

この例だとビルドすら要らないシングル HTML ファイルで動作しているが、このように外部 URL を指定してモジュールをインポートすれば、ビルド時に依存モジュールをバンドルする必要もなく、ほぼほぼ自分の書いたコードだけがファイルサイズを占める形となる。

使用するライブラリは CDN からダウンロードするので、ローカルマシン内でも CDN のドメインでキャッシュが効いて、他のサイトが同じライブラリを使っている時に容量を食わなくて済むんじゃないかしら。(最近のブラウザのキャッシュ事情知らないから間違ってたらゴメンナサイ;;)


Go、Deno、Rust (Cargo) あたりのモダンな言語では、依存パッケージをローカルマシンの一箇所に置いて、複数のアプリから共用できるような形で依存パッケージを管理している (同一アプリのバージョン違いなども適切に管理される)。Node.js がプロジェクトフォルダごとに node_modules/ ディレクトリを必要とするような作りとは違って、ローカルマシン内で消費する容量がうまく削減できていると思う。

Linux で aptdkpg が参照するレジストリはそうそうなくならない。Mac ユーザなんかは Homebrew に頼り切り、Windows ユーザも Chocolatey のレジストリが消失する可能性なんか考えもせず使っていることだろう。npmjs.comdeno.land/x/crates.io もずっと生き残り、同じ URL で同じモジュールを貞経し続けてくれるはずだ、とみんな信じている。実際、これら公式のレジストリは運営が多額の費用をかけて維持に努めているはずだから、実際のところ素人が心配するようなことはないんだろうと思ってはいる。

だが、Google Drive は容量無制限を止め、Heroku も無料枠をなくした昨今。npm は GitHub が吸収し、その GitHub は Microsoft の傘下になり資本関係は変わっている。今 CDN やホスティングサービス、無料でレジストラを運営してくれている団体が潰れるとも限らない。JavaScript といったら Node.js なんだろう、という時代も終わり、Bun が出てきたり様々な CDN がランタイムを独自開発したりしている。Go 言語は依存パッケージを GitHub リポジトリから取得できたりするのだが、もしも GitHub の方針転換でそれが機能しなくなったら、今動いているアプリはどうなるんだろう。


じゃあやっぱり依存モジュールもまとめてバンドルしてビルドするのが良いのかというと、そうとも思えない時がある。ブラウザや OS のアップデートに追従するため、アプリの機能的には変化がなくとも、仕方なく微修正を加えたりする時にビルドができなくなる場合があったりする。node-sass パッケージはちょっと古いバージョンになると内部で使うバイナリファイルが消失していてダウンロードできず、ビルド以前に npm install での環境構築が通らなくなっていたりする。コレって広く捉えると npmjs.com が未来永劫同じリソースを同じように愛知供してくれるとは限らない、って懸念にもなりそうだと解釈している。

実際、どこぞのライブラリの開発者は、金銭的支援が得られないからと自らパッケージを破棄したりしている。数年前にも left-pad パッケージが非公開になり、それに依存していた世界中のサービスが一時停止したりしていた。

そんな経緯もあり、npm の場合、最近は一度公開したモノを削除するのは難しくなっているものの、権威的なレジストラをもってしても、あの時はダウンロードできていたのに、もう今はダウンロードできなくなってしまったという事態はちょくちょく発生するのだ。


「忘れられる権利」「削除権」という問題もあるけども、今回は一旦それを脇に置かせてもらって、

というところをもっと発展させて、

というところをもう少し考えてみたい。


Underscore.js のサイトを見ると、複数の CDN サイトの URL が掲載されている。これらのどれでも、同じファイルが手に入るから、好きな CDN サービスを参照してね、というワケだ。


コレを利用して、近しいことを以前やったことがある。複数の URL の、どれか一つからファイルが読み込めれば OK として、上手く読み込めなかったら次の URL から DL を試す、というフォールバックを自分で実装したのだ。

端的にいえば、自分はこういうフォールバックの仕組みを標準仕様にしてほしいなと思っている。URL は未来永劫同じモノを指すワケではないし、ウェブサイトはすぐに消滅するし、単一のエンドポイントというモノをあんまり信用していないのである。


Deno では、次のように URL でパッケージ名とバージョン番号を指定してモジュールをインポートできる。

import { serve } from "https://deno.land/std@0.119.0/http/server.ts";

コレを見てパッと思い付いたのは、import 文に複数の CDN サービスの URL を列挙するというモノ。本稿執筆時点の ESM の構文的にはこんなモノ存在しないのだが、こんな風に書けたらどうだろうか。

// ↓ URL 自体はデタラメだからアクセスしないでねw
import ReactLikeExampleSPAFrameWork from [
  'https://jspm.dev/example-spa@18.0.0',
  'https://cdnjs.com/example-spa@18.0.0',
  'https://npmjs.com/example-spa@18.0.0',
  'https://unpkg.com/example-spa@18.0.0',
  'https://pagecdn.io/example-spa@18.0.0',
  'https://jsdelivr.net/example-spa@18.0.0',
  'https://cloudflare.com/example-spa@18.0.0'
];

from の後に配列で複数の URL を指定している。いずれも同じファイルのミラーに到達できる、というテイ。

指定された URL が本当に同じファイルなのかどうかチェックする仕組みがないと import に失敗しそうだとは思うので、ビルド時にハッシュ値とかを得ておいてそれを埋め込む、みたいにすれば開発者の想定するハッシュ値と照合してのフォールバックができたりするかな。

ただ、コレだと開発者が複数の CDN の URL を引っ張ってこないといけなくて、柔軟性がない。


ところで、最近の Node.js のバージョンでは、Node.js 組み込みモジュールに対して node: というプロトコルを指定して、組み込みモジュールを区別しやすくなった。

// 今までこう書いていたのが…
const fs = require('fs');
import fs from 'fs';

// `node:` というプロトコルを書けるようになった
const fs = require('node:fs');
import fs from 'node:fs';

コレを見て思ったのが、「任意の CDN から所定の形式の JS ファイルを取得する」ことを示すプロトコルがあれば、ブラウザが自動的に CDN からダウンロードしてくれるんじゃね?というモノ。

ブラウザには、予め SSL のルート認証局の証明書が登録されている。こういうノリで、ブラウザに予め「世の代表的な CDN」の情報を持っておいてもらう。そしてアプリ実装者は次のような import 文を書く。

import ReactLikeExampleSPAFrameWork from 'cdn-js:example-spa@18.0.0';

CSS を CDN からインポートしたかったらこんな感じ?

<link rel="stylesheet" href="cdn-css:example-css-library@4.0.0">

そうすると、ブラウザ内部では unkpg でも cloudflare でも、任意の「CDN リスト」から当該ファイルをダウンロードしてくる、という。「CDN リスト」は複数あるから、ファイルがないとか上手くその CDN に繫がらないとかいう時も自動的にフォールバックしてくれる、そんな感じ。

npmjs.com を自動的に取り込んでくれてるような CDN は良いが、それ以外に自分で用意したエンドポイントも指定したかったら、やっぱりさっきみたいに配列で from 指定できるようにしたらいいのかな?

import ReactLikeExampleSPAFrameWork from ['cdn-js:example-spa@18.0.0', 'https://example.com/example-spa@18.0.0'];

<script src><link href> の時にどうやってフォールバックを指定したらいいかは悩ましいけど、配列チックに書けたらいいのかな。w

<script src="[ cdn-js:example-spa@18.0.0 , https://example.com/example-spa@18.0.0 ]"></script>
<link rel="stylesheet" href="[ cdn-css:example-css-library@4.0.0 , https://example.com/example-css-library@4.0.0 ]">

picture 要素における source 要素みたいな入れ子でもいいのか?

<script src="cdn-js:example-spa@18.0.0">
  <source srcset="https://example.com/example-spa@18.0.0">
</script>
<link rel="stylesheet" href="cdn-css:example-css-library@4.0.0">
  <source srcset="https://example.com/example-css-library@4.0.0">
</link>

一番に読み込んでほしいのは srchref に今までどおり書いて、ネストした source srcset 側でフォールバック URL を任意に指定できる、みたいな。

とにかく、ベースは cdn-js: とか cdn-css: とかいうプロトコルをブラウザが実装して、そのプロトコルに合う形式で JS や CSS のライブラリを開発者が公開すれば、登録されている CDN サービスはミラーを自動的に作り、ローカルマシン内のキャッシュも単一で済む、ウェブアプリのコードとして読み込まれるのは本当に自分が実装した部分だけ、という感じにできそうだ。


ジオシティーズがなくなり、インフォシークがなくなり、個人的には「ウェブの生存期間なんて10年程度」みたいな感覚がある。だからできるだけ同一リソースのコピーをあちこちの権威的なレジストリが持つ仕組みになってくれないかなと。DNS サーバがキャッシュするのってそういう感じやん。

公開したモノが、後になって個人の意志で削除されることもある。この場合「削除権」をどう扱うか悩ましいところだが、JS フレームワークとか CSS ライブラリのコードをやっぱり消したくなる、っていう場面がどれくらいあるかなぁ。うっかりクレデンシャル情報あげちゃった時は消せるようにしておくべきだけど、プライバシーが漏洩するようなモノをライブラリのコードに書くかな、っていう。OSS 開発でお金もらってるような生計の立て方もあるみたいだから、そういう人達は金銭的な兼ね合いから非公開にしたいとか思うのかもしれんけど、僕は古いインターネットの価値観の人間なので、「ネット上に上げたモノは全部無料で共有すればいいだろ」「無断コピーされて当然・上等」「閲覧時に必ずダウンロードされるんだから、後から完全削除なんかできっこない」っていう考えがずっとある。もう今の時代こういう発想はウケないんだろうなと自覚はしている。でも僕が望むのはそういうインターネットだったりする。権利だ利益だってケチケチしないで全部無料公開してみんなで共有しようや。的な。

ホンマに deno.land に依存してていいのかなぁ、かといって全部バンドルするのもダルいで、ローカルマシンでのキャッシュを上手く効かせられんかね、色んなサイトが React みたいな同じ「資産」を共通的に利用してるんだから共用・共有はできんかね、的なことをぼんやり考えました。おしり。