◆ 便利だけど困ったところも色々

すでに minify の設定方法とかサブディレクトリにビルドファイルを配置するときの問題とか書いてますが 書くことが色々あったので今回は少しまとめて書きます

parcel 使ってます

最近は WebComponents を主に使ってみてるのですが コンポーネントいうだけあって細かな機能に分けていっぱいコンポーネントがあります
基本絶対にまとめて使うものでもなければコンポーネントごとに .js ファイルを作るのでファイルがいっぱいになります
多くなってくるとロードが遅いなと感じることがあるので parcel でバンドルすることにしました

バンドルといえば webpack が有名ですが設定大変そうという印象しかなくてまず手を付ける気がしません
parcel ならエントリポイントのファイル index.html とか index.js とかを指定すれば後は勝手にやってくれます
ただ設定なしを謳ってるだけあって細かな設定はできないものが多めです
一応 .babelrc や .postcssrc ファイルを作って minify の設定やプラグインの機能を使えるようにしたりできます
package.json からも browserlist などが使用されます

私の場合はバンドルだけしてくれればよくて babel の変換は不要なので Chrome の最新 2 バージョンを指定してます
source map は使っていてちゃんと動かないことが多いのでオフにして minify なしバンドル結果のコードをそのままみています
Chrome 向けならほぼ変換不要なのですが一応 babel は通されるのでセミコロンがついたり微妙に改行位置が変わったりします

hmr があんまり意味ない

hot module replacement という機能がついてるようで開発用に watch しているとソースコードに変更があったときにブラウザの方も自動で更新してくれます
中身は WebSocket を使って通信してました
単にファイル監視してビルドするだけじゃなくて WebSocket サーバまで起動してるようです

この機能は一見便利なのですがリロードをしないで JavaScript や CSS のファイルを読み直してるだけです
JavaScript なら 2 回定義できないものを定義してエラーになるので結局手動でリロードしています
const やクラス定義は 2 回目はエラーになりますからね
私の場合は customElements.define で定義済みですとエラーがよくでています

これらを使ってなくてもリスナ設定があれば 2 度リスナがつけられますし JavaScript を置き換えるのは純粋な関数のみ詰め合わせたライブラリ部分でもないと意味ないと思います

CSS の場合は基本的に問題なかったのですが せっかく postcss がついているのでと css modules を使ってみるとこれまた使えなくなりました
css modules では同じ id やクラス名でも重複しないようにランダムな文字列がつけられてたりして一意なものになります
css ファイルをインポートすると変換済みの id やクラス名を取得できるので

import css from "./a.css"

document.body.classList.add(css.body)

のように使えます

ちなみに中身が全く同じだと別ファイルでも同じランダムな文字列になっていたので 内部のハッシュ値とかが使われてそうです
バンドルごとに変わるのでその時の時刻やランダム値+内部の値なのかもしれません
ただ全く同じのがバンドルファイルに 2 回登場したので無駄なものをカットしてはくれないようです

と言う感じで css modules を使うとランダムな値が付けられるのですが CSS を更新したら CSS 側のクラス名などは変わるのに HTML についてるクラス名はそのままなのでスタイルが反映されなくなってしまいます
なのでやっぱりリロードが必要で hmr が意味ないなと思えました

... が使えない

これは parcel というより babel の問題なのですが

const obj = {a: 1, b: 2}
const obj2 = {...obj, b: 3}

これがエラーになります

×  C:\dev\parceltest\01\index.js:7:14: Unexpected token (7:14)
5 |
6 | const obj = {a: 1, b: 2}
> 7 | const obj2 = {...obj, b: 3}
| ^
8 |
9 |

「...」 が未対応で Unexpected token と言われます

Chrome では結構前から使えてたと思うのですが仕様的には ES2018 の機能です
babel のプラグインリストでは ES2017 までなのでその機能は Experimental の babel-plugin-transform-object-rest-spread というものになります
この名前のパッケージを npm からインストールして

{
"plugins": ["transform-object-rest-spread"]
}

という .babelrc ファイルを入れると使えるようになるのですが babel をこれまで使ってないと少し面倒です
私の場合は変換不要なので .babelrc ファイルすらなかったですし これをいれるだけで node_modules フォルダに色々 babel 系のものがインストールされて重くなりました
それに入れたところでブラウザの機能は使われず 変換されることになってしまいます

「{ "useBuiltIns": true }」 オプションをつけないと Object.assign すら使わず polyfill 関数を使うのでいらないライブラリまでバンドルに含まれてしまいます
また Object.assign するだけではできない いらないプロパティを除外する使い方をすると それ以外をプロパティだけの新しいオブジェクトを作るために for-in が実行されていたりで 変換してほしくないから Chrome のみにしてるのになんかなぁって気分になりました

あと最小限で作ったプロジェクト中では問題なく使えたのですが 少し大きめのプロジェクトでは有効にならないみたいで Unexpected token が出続けました
.babelrc ファイルはコピペですし yarn でインストールしたのも一緒です
package.json は大きいプロジェクトではこのファイルも大きめですが関係なさそうな scripts や依存パッケージとかくらいなのになぜかダメでした
競合して有効にならないような設定やパッケージでもあったのでしょうか

どっちにしても余計な変換されるなら標準に入るまでは使わずに書くことで対処するようにしました

バンドル用の変換のみで他のコードには一切触れなくてシンプルなバンドラーがほしいです
rollup って webpack みたいななんでもまとめるじゃなくて ES Modules の代替みたいなものって聞いた気がするけどそういうのなのかな?

dynamic import

これはまだ stage-3 だったと思いますが条件付きで動きました
import 関数の中がリテラルでインポート対象が確定してるときのみ変換されます

function dynamicImport(name){
return import(name)
}

function dynamicStaticImport(name){
switch(name){
case "foo": return import("./foo.js")
case "foo": return import("./bar.js")
case "foo": return import("./baz.js")
}
}

というコードは
⇩ のように変換されました

function dynamicImport(name) {
return import(name);
}

function dynamicStaticImport(name) {
switch (name) {
case "foo":
return require("_bundle_loader")(require.resolve("./foo.js"));
case "bar":
return require("_bundle_loader")(require.resolve("./bar.js"));
case "baz":
return require("_bundle_loader")(require.resolve("./baz.js"));
}
}

引数を元にインポートするのは事前には何をインポートするかわからないので仕方ないのかもです
ビルド時にわからないならバンドルに含ませることもできませんし
それでもバンドルに含まれてるなら import できるようになっていてほしかったのですけどダメでした

import 関数は parcel のものではないのでそのままブラウザ import 関数が実行されてるようで URL にリクエストして見つからないエラーでした

yarn loop

parcel は必要なパッケージが見つからないと自動でインポートしてくれます
.babelrc ファイルや .postcssrc ファイルにモジュールを追加したり minify に必要なモジュールがないときなどです
最近は parcel が使ってる cssnano が 4 にメジャーアップデートしてバンドル時にはユーザのものを使うようになったらしく入ってないならビルド時にインストールされます

便利な機能なのですが困ったことにこの自動インストールは無限ループしていつまで経っても完了しません
yarn add のところでステップが最後まで完了したらまた yarn add が始まって という感じです
どれだけ待ってもずっと繰り返しています

強制終了して 自分で yarn install でインストールを完了して再度ビルドするとうまくいきます
たまに中途半端にインストールされてしまったのか これでもダメで .cache フォルダと node_modules フォルダを削除して yarn install してからビルドするとできたときもありました

画像の import

css から url でインポートするときは問題なかったのですが JavaScript で import するとパスの問題がでました
ソースマップと同じで public-url が相対パスで設定されます

--public-url app

なら import で app/icon.png のようなファイル名を受け取ります
base タグがあるので /app/app/icon.png へアクセスしようとしてエラーになります
かと言って public-url をなくすと /icon.png と絶対パスになるので base タグの効果はなくエラーになります
解決策もソースマップのものと同じで

--public-url .

でカレントディレクトリを public-url に設定するとカレントディレクトリの icon.png をロードしようとするので base タグの効果で思い通りのファイルをロードできます

除外できない

JavaScript ファイルから JavaScript ファイルをインポートするときはすべてバンドルファイルに含めてもいいのですが HTML や CSS からロードするファイルはバンドル対象としたくないときがあります
一部ライブラリや画像ファイルなどはすでに公開用フォルダに置いていて通常のロードにしたいということは結構あると思います

特に画像ファイルの場合は base64 でスクリプト中に含まれるということはなく個別の画像ファイルになるので サーバ上で他からも使われてるならビルド対象フォルダにも同じファイルを入れてビルドされた別名ファイルでサーバ上に複数あるというのは避けたいです
ビルド時にファイルのチェックがあるので間違ったパスや消してしまったときに事前にわかるなどのメリットはありますが 書き換えた時にサーバ全体で変わってほしいなどがあると別々に管理したくないです

parcel だとそういう除外設定がないので無理なのかと思ってましたが index.html をビルド対象にするのではなく index.js をビルド対象にして index.html から index.js をロードするようにすれば解決できました
index.html には別々にロードしたいライブラリを書いて バンドルされた index.js も読み込みます

<!doctype html>

<script src="/lib/foo.js"></script>
<link rel="stylesheet" href="/lib/bar.css">
<script src="index.js"></script>

JavaScript 中から画像ファイルをロードするときはただの文字列なので自分で import しない限りは好きな URL のものを直接ロードできます
ただ CSS の url(img.jpg) はバンドルに含めるしかないようで 絶対変換されてしまいます
postcss のモジュールでなんとかできるのかもしれませんが parcel も関わってきてるので複雑そうなので諦めました

画像限定ですし index.html でロードするバンドル外の CSS ファイルで画像 URL を指定した CSS をロードしておくとかでいいと思います
そもそもバンドル対象は JavaScript だけでよくて CSS は css modules とか使わない限りグローバルの 1 つだけや ShadowDOM 内のインライン style タグだけで十分ですしね

this.attachShadow({mode: "open"}).innerHTML = `
<style>
.x {background: url("/path/to/img.png");}
.y {background: pink;}
</style>
<div class="x">
<p class="y"></p>
</div>
`

こうなると JavaScript 中の文字列なので変換はされません

相対パス

インポート時に node_modules は直接

import {html, render} from "lit-html"

と書けますがそれ以外のローカルファイルは相対パスになります
ただそれだと

import CommonElement from "../../../lib/components/common-element.js"

みたいな分かりづらいパスになります
これだとライブラリ部分は変えなくても自分が移動しただけでパスも修正が必要です
同じフォルダやサブフォルダなど相対的なパスが良いケース以外では 相対パスは書きたくないです

バンドルしない EsModules ならサーバルートからの絶対パスにできますが バンドル時の絶対パスはファイルシステムのルートになってプロジェクトフォルダの位置に左右されるのは困ります

良い方法がないか探していると parcel ではプロジェクトルートを指す ~ がサポートされていて

import "~/lib/components/tab-element.js"

のように書けます
プロジェクトルートは package.json の場所かと思ったのですがエントリポイントのファイルのフォルダでした
なので package.json を置くフォルダに src フォルダを作ってそこにソースを入れていたとしても 「~/src/」 を毎回書かなくて済みます

また

app1/
index.js
app2/
index.js
common/
lib.js

と複数エントリポイントがあって別のフォルダにライブラリを置いてるときに

import "~/../common/lib.js"

と書けます
polymer か何かのビルドツールはプロジェクトのルートフォルダより上にはアクセスできない 使いづらい仕様だったのでこれは結構便利です