Vapor Trail

明るく楽しく元気よく

JetBrainsのIDEでhuskyが動かない

環境

Ubuntu 20.04
Node.js v14.7.0
npm v6.14.8
nvm v0.35.3

package.json
{
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject",
    "type-check": "tsc --pretty --noEmit",
    "lint": "eslint **/*.{ts,tsx}",
    "format": "prettier --write **/*.{ts,tsx} "
  },
  "dependencies": {
    "expo": "~39.0.2",
    "expo-status-bar": "~1.0.2",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.4.tar.gz",
    "react-native-web": "~0.13.12"
  },
  "devDependencies": {
    "@babel/core": "~7.9.0",
    "@types/react": "~16.9.35",
    "@types/react-dom": "~16.9.8",
    "@types/react-native": "~0.63.2",
    "@typescript-eslint/eslint-plugin": "^4.5.0",
    "@typescript-eslint/parser": "^4.5.0",
    "eslint": "^7.12.0",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-react": "^7.21.5",
    "eslint-plugin-react-hooks": "^4.2.0",
    "husky": "^4.3.0",
    "lint-staged": "^10.4.2",
    "prettier": "^2.1.2",
    "typescript": "~3.9.5"
  },
  "private": true,
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.@(ts|tsx)": [
      "lint",
      "format"
    ]
  }
}

エラーがあるのにコミットできてしまう

治安を守るためにlint-staged+huskyを入れてeslintのエラーがある場合はコミットできないように設定をしたが、試しにエラーが出るようにファイルを編集してコミットしてみても普通にコミットできてしまう。

しかもVSCodeではコミットできないがPhpStormでは普通にコミットできてしまう。

gitのコンソールを見るとnpxのパスが見つからないということらしい。

Can't find npx in PATH: /usr/lib/git-core:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
Skipping pre-commit hook

github.com

~/.huskyrcを作成して設定したらPhpStormでも動作するようになった。

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

huskyのシェルでnvmの設定が読み込めてなかっただけらしい。

そもそも.zshrc環境変数を設定をしていたことが間違いで、husky.shでもnvmのPATHが読み込めるように./husky.rcも削除して素直にきちんと~./profileに設定するようにしました。

QRコードでのアプリ起動とユニバーサルリンク

QRコードを読み込んでアプリがインストール済みの場合はアプリを起動し、未インストールのときはストアもしくは特定のURLをブラウザで開くようにしたい。

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <title>Starting App...</title>
    <script>

        var URL = "example-app://~~"; //Android 起動して表示したいアプリのページ
        var MARKET = "market://details?id=com.example";
        var iTunes = "itms://itunes.apple.com/app/idxxxxxxx";
        var QR = "http://example.com/qr";
        var UniversalLink = "https://example.com/app-page"; //iOS 起動して表示したいアプリのページ

        function onLoad() {
            if (navigator.userAgent.match(/Android/)) {
                if (navigator.userAgent.match(/Chrome/)) {
                    setTimeout(function () {
                        if (!document.webkitHidden)
                            window.location.href = MARKET;
                    }, 1000);
                    window.location.href = URL;
                } else {
                    var iframe = document.createElement("iframe");
                    iframe.style.border = "none";
                    iframe.style.width = "1px";
                    iframe.style.height = "1px";
                    var t = setTimeout(function () {
                        window.location = MARKET;
                    }, 1000);
                    iframe.onload = function () {
                        clearTimeout(t)
                    };
                    iframe.src = URL;
                    document.body.appendChild(iframe);
                }
            } else if (navigator.userAgent.match(/iPhone|iPad|iPod/)) {
                setTimeout(function () {
                    if (!document.webkitHidden) {
                        window.location = iTunes;
                    }
                }, 25);
                window.location = UniversalLink;
            } else {
                // PCで開いた場合はQRコードを表示
                var img = document.createElement("img");
                img.src = "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=" + encodeURIComponent(QR);
                document.body.appendChild(img);
            }
        }
    </script>
</head>
<body onload="onLoad()">
</body>
</html>

ios - How to fall back to marketplace when Android custom URL scheme not handled? - Stack Overflow を参考にQRコード読み込み先のページに上記のhtmlを設置した。

  1. QRコードを読み込み
  2. ブラウザが起動して、上記のHTMLを読み込み
    2-1. アプリインストール済みの場合はアプリが起動
    2-2. アプリ未インストールの時はストアページが開く

Androidは上記の処理で問題なく動いたが、iOSのユニバーサルリンクはリンク元とリンク先が同一ドメインだと使えないという罠があった。

ユニバーサルリンクはリンクを開いた時にインストール済みのアプリが起動する仕組み。 リンクをクリックすると https://リンク先.com/apple-app-site-association 1 に設定してあるリンクのpathに対応してAssociatedDomainsのリンクに登録されたアプリが起動する。

たとえばAssociatedDomainsにapplinks:example.comと設定されたアプリをインストールしてAというサイトでexample.comのリンクを踏むとhttps://example.com/apple-app-site-association の設定をもとにアプリが起動する。
下記の例はpathsを"*"に設定してexample.comのどのパスでも起動するようにしているが、URLに特定のパスが含まれるときのみアプリを起動させたりすることもできる 2

apple-app-site-association.json

{
    "applinks": {
        "apps": [],
        "details": {
            "チーム名.com.example": {
                "paths": [
                    "*"
                ]
            }
        }
    }
}

ただiOSの場合はリンク元ドメインとリンク先のドメインが同一だと起動しない仕組みになっており、example.com内のページをSafariで閲覧中にexample.com/~~のリンクを踏んでもアプリは起動しない。

When a user is browsing your website in Safari and they tap a universal link to a URL in the same domain as the current webpage, iOS respects the user’s most likely intent and opens the link in Safari. If the user taps a universal link to a URL in a different domain, iOS opens the link in your app.
ユーザーがSafariでWebサイトを閲覧しているときに、現在のWebページと同じドメインのURLへのユニバーサルリンクをタップすると、iOSはユーザーの最も可能性の高い意図を尊重し、Safariでリンクを開きます。ユーザーが別のドメインのURLへのユニバーサルリンクをタップすると、iOSはアプリでリンクを開きます。

App Search Programming Guide: Support Universal Links

普段iPhoneを使用していないので知らない挙動だったが、ブラウザで閲覧中にURLをタップしても勝手に外部アプリが起動しないようにという配慮らしい。

今回の場合QRコードの読み込み先としてhttps://example.com/~~にアクセスさせて、さらにユニバーサルリンクとして設定したhttps://example.com/app-page にリダイレクトしてアプリを起動するはずだったが、リダイレクト元とリダイレクト先のドメインが同一なのでアプリが起動しなかった。

そのためサブドメインを使用してAssociatedDomainsに設定しているapplinksをexample.comからsub.example.comに変更し、apple-app-site-associationはhttps://sub.example.com/apple-app-site-associatioに設置。 QRコードの読み込み先URLはhttps://example.com/~~ に設置しておいて、https://sub.example.com にリダイレクトさせるようにした。ややこしい。

参考: [Swift] 同一ドメインでのユニバーサルリンク(Universal Links)は動作しないので注意 - Qiita


  1. アプリインストールor更新時にダウンロードされるらしい。https://developer.apple.com/forums/thread/6972

  2. メルカリの設定例: https://www.mercari.com/apple-app-site-association

VSCode+ESLint+Prettierの設定覚書

Reactで快適に開発するために初期設定をいろいろ試した。 とりあえず余計なものは極力入れないようにした。コーディングルールもrecommendに従う。

ESLintとPrettierをなぜ両方入れるのかよくわかってなかったのでこちらを参考にした。

qiita.com

ESLint と Prettier の共存設定とその根拠について

設定

1. VSCodeでESLint,Prettierプラグインをインストール

2. プロジェクト内で関連モジュールをインストール

$ npm i -D eslint prettier eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks

3. 設定ファイルを書く

.vscode/settings.json

{
  // formatter
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.formatOnType": true,
  // eslint settings
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true // eslint
  }
}

.eslintrc

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "sourceType": "module"
  },
  "plugins": ["react", "react-hooks"],
  "rules": {
    "react/jsx-uses-react": "error",
    "react/jsx-uses-vars": "error",
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

.prettierrc

{
  "endOfLine": "lf",
  "singleQuote": true
}

4. commit時にlintするようにgit-hookを設定

$ npm i -D husky lint-staged

package.json

{
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject"
  },
  "dependencies": {
    "expo": "~39.0.2",
    "expo-status-bar": "~1.0.2",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
    "react-native-web": "~0.13.12"
  },
  "devDependencies": {
    "@babel/core": "~7.9.0",
    "eslint": "^7.10.0",
    "eslint-config-prettier": "^6.12.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jsx-a11y": "^6.3.1",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-react": "^7.21.3",
    "eslint-plugin-react-hooks": "^4.1.2",
    "husky": "^4.3.0",
    "lint-staged": "^10.4.0",
    "prettier": "^2.1.2"
  },
  "private": true,
  "plugins": [
    "react",
    "jsx-a11y",
    "import"
  ],
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  }
}