Universal Linksの設定をテストするNPMパッケージ「universal-links-test」をOSSとして公開しました

Universal Linksの設定をテストするNPMパッケージ「universal-links-test」をOSSとして公開しました

はじめに

こんにちは、WEAR Webフロントエンドチームでテックリードをしている冨川(@ssssota)です。業務でUniversal Linksのテストを効率化するために、独自のパッケージを開発し、GitHubおよびnpmで公開しました。本記事ではそのモチベーションと利用方法などを紹介します。

目次

背景・課題

まず、Universal Linksについて紹介します。Universal Linksは、ブラウザなどからiOSアプリを開くためのディープリンク技術の一種です。apple-app-site-association というファイルをWebサーバに配置することで、WebページとiOSアプリの関連付けを行います。

apple-app-site-association ファイル

apple-app-site-association ファイルは、Webサーバに配置するJSONファイルです。このファイルには、Universal Linksの挙動を制御するための情報を記述します。以下に例を示します。

{
  "applinks": {
    "details": [
      {
        "appID": "HOGE1.com.example.app",
        "components": [
          { "#": "nondeeplinking", "exclude": true },
          { "/": "/search/", "?": { "q": "*" } }
        ]
      },
      {
        "appIDs": ["HOGE2.com.example.app", "HOGE2.com.example.app"],
        "components": [{ "/": "/*/posts/*" }]
      }
    ]
  }
}

このように、 apple-app-site-association ファイルには、アプリIDと対象URLのマッピングが記述されています。このファイルをWebサーバに配置することで、対象URLに対してiOSアプリが開かれるか否かを制御できます。一見するとglobのような記述と、パス、クエリ、フラグメントを設定でき柔軟に見えます。しかし、挙動が予想しにくい面もあります。

例えば、 * はワイルドカードで / という文字にもマッチします。上記の例では /*/posts/* というパス設定を記述していますが、 /foo/posts//foo/bar/posts/baz/qux なども該当します。これはglobに慣れているエンジニアにとっては違和感を感じるかもしれません。

挙動確認の課題

Webサーバにファイルを配置する関係上、アプリエンジニアではなくWebエンジニアがファイルを管理することがあります。これはWebアプリのパスを管理しているエンジニアが管理するという意味では非常に合理的ではあるものの、挙動確認はやや困難です。設定のミスによって、意図せずアプリが起動するようになるなどのリスクもあります。

apple-app-site-association ファイルの挙動確認は、 swcutil というmacOSにプリインストールされたコマンドを用いることで行うことができます。しかし、このコマンドはドキュメントが少ない他、Universal Linksの挙動確認においては使い勝手が悪いです。以下にいくつかの使用例を示します。

$ swcutil verify -j ./apple-app-site-association -u '/'
swcutil must be run as root
$ sudo swcutil verify -j ./apple-app-site-association -u '/'
{ s = applinks, a = HOGE.com.example.app, d = www.example.com }: Pattern "/" matched.
$ sudo swcutil verify -j '{"applinks":{"details":[{"appIDs":["HOGE1.com.example.app","HOGE2.com.example.app"],"components":[{"#":"nondeeplinking","exclude":true},{"/":"/search/"}]}]}}' -u '/search/'
{ s = applinks, a = HOGE1.com.example.app, d = www.example.com }: Pattern "/search/" matched.
{ s = applinks, a = HOGE2.com.example.app, d = www.example.com }: Pattern "/search/" matched.

まず、 sudo コマンドでroot権限を使用していることに気づきます。非root権限で実行すると swcutil must be run as root というメッセージが標準エラー出力に出力されます。また、規模の大きなWebアプリケーションでは多数のURLパスのテストが必要となるケースも考えられます。このような場合シェルスクリプトなどを用い反復することになります。しかしながら出力はプログラムから扱いやすい形式とはいえません。

解決の取り組み

先のような課題を解決するため、 universal-links-test というNPMパッケージを開発しました。このパッケージは、 swcutil コマンドをラップした関数を提供し、 apple-app-site-association ファイルの挙動確認をサポートします。

NPMパッケージとして提供することで、Web開発者が容易に導入し、テストと共に apple-app-site-association ファイルを管理できるようになります。

パッケージの機能

現在、universal-links-test は主に verify 関数を提供します1。この関数は apple-app-site-association ファイルのファイルパス(もしくはJSON)とURLを受けとり、指定されたURLによってどのアプリが開かれるかをMapで返します。以下に例を示します。

import { verify, type AppleAppSiteAssociation } from "universal-links-test";

const aasa: AppleAppSiteAssociation = {
  applinks: {
    details: [{
      appIDs: ["HOGE.com.example.app"],
      components: [
        { "#": "nondeeplinking", exclude: true },
        { "/": "/search/" }
      ],
    }],
  },
};
const result: Map<string, "match" | "block"> = await verify(aasa, "/search/");
console.log(result.get("HOGE.com.example.app")); // => "match"

root権限とmacOS依存

verify 関数は swcutil コマンドのラッパーであるため、macOSかつroot権限が必要です。 universal-links-test はこの課題に対し完全な解決策は提供できないものの、緩和策を提供します。

それが universal-links-test/sim というモジュールです。これは universal-links-test 同様、 verify 関数を提供しています。 swcutil の挙動をシミュレートしているため、macOSやroot権限でなくとも利用できます。開発時には universal-links-test/sim を、CI環境では universal-links-test を利用することで、Universal Linksの挙動確認を確実に行えます。

GitHub Actionsでの利用例を以下に示します。 universal-links-test はmacOSかつroot権限を必要とするため、CIはmacOSランナーを利用する他、 sudo コマンドを用いてroot権限を取得する必要があります。すべてのテストをroot権限で実行するのはセキュリティ上望ましくないため、VitestやJestなどのテストランナーを使っている場合でも、対象ファイルのみ sudo で実行するなど配慮が必要です。以下の例では node --test コマンドを使うことで隔離しています。

// universal-links.test.js
import { verify } from "universal-links-test";
import { test } from "node:test";
import * as assert from "node:assert";
const appleAppSiteAssociationPath = "path/to/apple-app-site-association";
test("Universal Links test", async () => {
  const cases = [
    ["HOGE.com.example.app", "/search/", "match"],
    ["HOGE.com.example.app", "/search/#nondeeplinking", "block"],
    ["HOGE.com.example.app", "/", undefined],
  ];
  for (const [appID, path, expected] of cases) {
    const result = await verify(appleAppSiteAssociationPath, path);
    assert.strictEqual(result.get(appID), expected);
  }
});
# .github/workflows/test.yml
# ...
jobs:
  test:
    runs-on: macos-latest # macOSランナーを利用
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: sudo node --test universal-links.test.js # root権限が必要

デモ

デモページをGitHub Pagesで公開しています。このページは universal-links-test/sim を利用しているため、ブラウザから環境問わずUniversal Linksの挙動確認できます。

おわりに

universal-links-test は、Universal Linksの挙動確認をサポートするためのNPMパッケージです。Web開発者が容易に導入し、 apple-app-site-association ファイルの管理と挙動確認ができます。また、 universal-links-test/sim を利用することでmacOSやroot権限がなくともUniversal Linksの挙動確認ができます。

universal-links-test では、今後も機能追加やバグ修正していく予定です。ぜひご利用いただき、フィードバックをお寄せいただければ幸いです。

私たちZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com


  1. v0.1.0リリース時点での機能です。今後のバージョンで機能変更される可能性があります。
カテゴリー