◆ url.path はない
◆ url.pathname は search などを含んでない
◆ url.href から url.origin の除去は username:password に対応できない
◆ pathname + search + hash は ? や # が省略されて元と異なる場合がある

new URL("http://foo.bar/baz?x=y#z")

で作る URL オブジェクトですが パスを取得しようとするとかなり面倒でした

ここでの「パス」は上の URL でいう 「/baz?x=y#z」 の部分で ? や # も含んだものです

URL オブジェクトに用意されている pathname で取得できるのは 「/baz」 までです

基本的にはこの 2 パターンです

const url = new URL("http://foo.bar/baz?x=y#z")

const path1 = url.href.slice(url.origin.length)
path1
// /baz?x=y#z

const path2 = url.pathname + url.search + url.hash
path2
// /baz?x=y#z

path1 === path2
// true

path1 の方ですが URL 全体から origin 部分がいらないので href から origin を削除します
完全な URL は origin から始まるので単純に origin の文字数で slice しています

path2 の方ですが ほしい部分は pathname と search と hash の部分の組み合わせなので これらを結合しています

そこまで長くないとは言え 毎回書くのは面倒です
url.path で取れたらいいのですけど

正しくない場合がある

長いだけではなく 正しい URL にならない例外もあります

path1

まずは path1 のケースです
URL には basic 認証用のユーザ名やパスワードが入る場合があります
場所はホスト名の前で origin の中になるのですが origin では取得できません

const url = new URL("http://user:pass@foo.bar/baz")
url.origin
// http://foo.bar
url.href
// http://user:pass@foo.bar/baz

origin の文字数に認証情報分が足りなくなるので path1 の方法だと origin の後半が path に含まれてしまう場合があります
単純に origin の長さに url.username と url.password の長さを足すだけで解決できるものでもありません
username や password に : や @ は含まれないので

http://foo.bar/
http://@foo.bar/
http://:@foo.bar/

http://user@foo.bar/
http://user:@foo.bar/

こういうケースでおかしくなります

path2

また path2 のケースですが こっちも省略して記号だけの場合に元の URL のものと一致しなくなります

const url = new URL("http://foo.bar/baz?#")
url.search === ""
// true
url.hash === ""
// true

通常は search や hash に ? や # は含まれますが ? や # だけの場合は空文字です
もちろん元 URL に ? や # 自体がない場合も空文字です
つまり ? や # があったのかどうかは判断できません

URL 的には同じものを指すので 基本的には問題なく 不要な ? や # を消したいときにはむしろ嬉しいのですが 完全に同じものにする必要があるときには使えません

対策

? や # の有無は href にしかないので href を使うしかないのですが href でも

http://foo.bar

みたいな場合に自動で / が追加されてしまいますし エスケープされて元と異なるケースがあります

パース結果を基に元の URL から取り出そうといろいろ考えてはみました

const index = original_url.href.indexOf(url.pathname, url.protocol.length + 2)
const path = original_url.href.slice(index)



const path = original_url.match(new RegExp("@?" + regexpEscape(url.host) + "(.*)"))[1]

や search や hash が空の場合に 元 URL から ? や # があったかどうかを判定などです

しかし何かと例外的なパターンが出てきます

最終的に

そもそもこんな特殊な対応が必要なのがおかしいと思うんです
特殊なパターンは一切無視で

url.pathname + url.search + url.hash

を採用しました

  • 必要部分はエスケープ
  • ? や # だけなら省略する
  • pathname の / は省略不可で必須

という制限が付きますが URL オブジェクトがエスケープするようなものなら必須なエスケープのはずです
? や # だけの有無で動作を変える作りがおかしいです

そういうのをなくすためにも余計な配慮はしないことにしました

それでも 3 つくっつけないといけないので面倒なんですよね
url.path ができてほしいものです