npm link なにが便利か

tl;dr

手元で開発中のモジュールをローカルの別のプロジェクトで使いたいときに便利


  • いまあなたは『my-great-module』というすごい便利なモジュールを作っているとする

  • すごい便利なモジュールなのだけど一部未完成なのでnpmにパブリッシュするのはまだためらっている

  • でもすごい便利なモジュールなのでβでもいいから手元で開発中の別のプロジェクトで早く使いたい

    • なんとかしたい

みたいなときに便利。

もしくは、

  • 公開中のnpmモジュールに新機能付けたいけどドッグフーディングしたい
    • まだnpmに公開はしたくなくて、ローカルで試したい
      • ブランチ切って試したい

みたいなときに便利。

npm link

docs.npmjs.com

使い方書いてある。

使い方ザッと書くと

  1. 利用元のモジュールへ cdして npm link
$ cd my-great-module
$ npm link
  1. 利用先で npm link <モジュール名>
$ cd my-develop-other-project
$ npm link my-great-module

注意点

  • npmのグローバル(npm -g)にシンボリックリンク貼って参照している
  • npm link <モジュール名>
  • スコープモジュールの場合は @scope/モジュール名形式
    • 要は npm i するときと同じ形式でlinkする

yarmもあるよ


ようは自前のnpmモジュールを手元でシュッと試したいときとかに便利ですね

TypeScriptのコンパイル時に『Cannot find name 'EventListenerOrEventListenerObject'.』と怒られる

Firebase Cloud FunctionsをTSで書いて使おうとしていて $ firebase init functions したあとにとりあえずHello Worldしようと思ってハマった。

以下のgetting-startedをのindex.tsをコピーして

github.com

$ firebase deploy --only functions

したところtscコンパイルでいきなりエラーが出てしまった。

node_modules/@types/history/DOMUtils.d.ts:2:78 - error TS2304: Cannot find name 'EventListenerOrEventListenerObject'.

こう怒られる

../node_modules/@types/react-dom/index.d.ts:19:72 - error TS2304: Cannot find name 'Text'.

加えてこうも怒られる

解決手段

tsconfig.json"lib""dom"を足す

こんな感じ

{
  "compilerOptions": {
    "lib": ["es6","dom"], ← "dom"を足す
    "module": "commonjs",
    "noImplicitReturns": true,
    "outDir": "lib",
    "sourceMap": true,
    "target": "es6"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}
参考情報

JavaScriptでオブジェクトの分割代入をするとき変数名を別の名前にする

オブジェクトの分割代入(Destructuring assignment)をするときに、変数名を自分の好きな名前に変更したいと思って調べたら、方法があった。

MDN

developer.mozilla.org

異なる名前を持つ変数への代入

var o = {p: 42, q: true};
var {p: foo, q: bar} = o;
 
console.log(foo); // 42 
console.log(bar); // true  

var { キー名 } = オブジェクトの形式で分割代入するところをvar { キー名: 任意名 } = オブジェクトとすることでOK。

このMDNの例だと

var { p } = o

var { p: foo } = o

としてpをfooに変更している。


なんだか慣れないとオブジェクトのkey: valueの構文もしくは型指定に見えてきてしまうので早く慣れたい。

ESモジュールのimport構文でimport { p as foo } from 'hoge' という構文があるから、asを使ったこのノリでできないのがなんでなんだろうなーという気がしている。深く調べてないのでECMAの仕様決定までの流れをいつか調べたい。

Reactでフォームの外側あるいは別コンポーネントのbuttonからsubmitする

FormikというReactのフォームライブラリがあり、それでFormを組んでいてハマったのでメモ。

github.com


欲求

Formの中にButtonを配置せず、『外側にサブミットボタンを配置したい』

ようはフォームの外側にonSubmitなボタンコンポーネントを作って、それを押したら目的のフォームがsubmitされるようにしたい。

解決策

github.com

react-final-formのFAQみてたら書いてあった。

① formのDOM取得してEventをディスパッチする

Via document.getElementById()

<button onClick={() => {
  document.getElementById('myForm').dispatchEvent(new Event('submit')) // ✅
}}>Submit</button>

<form id="myForm" onSubmit={handleSubmit}>
  ...fields go here...
</form>

なるほど!

クロージャでsubmt変数を定義してそこにsubmit関数を代入する

Via Closure

let submit
return (
  <div>
    <button onClick={submit}>Submit</button> // ❌ Not overwritten closure value
    <button onClick={event => submit(event)}>Submit</button> // ✅
    <Form
      onSubmit={onSubmit}
      render={({ handleSubmit }) => {
        submit = handleSubmit
        return <form>...fields go here...</form>
      }}
    />
  </div>
)

たしかに〜

③ そもそもHTML5のform属性を利用できる

これは別途react-final-formのFAQには書いてないけどやってみたらできた別案。

HTML5にはform属性があって、例えばMDNのbuttonのページにはこうある

ボタンに関連付けられた form 要素(form owner)です。 属性値は同一文書内の

要素の id 属性と同一の値にしなくてはなりません。 この属性を指定しない場合は、祖先に 要素が存在すれば、その要素に関連付けられます。 この属性によって 要素の子孫にするだけでなく、同一文書内にある任意の <form> 要素に <button> 要素を関連付けることが可能になりました。

ようするにformのid属性と関連付けが出来る

ので、こう書けばformの中にbuttonがなくともsubmitが可能

<button form='myform'>Submit</button>

<form id="myForm" onSubmit={handleSubmit}>
  ...fields go here...
</form>

ReactでFormむずかしい

アプリっぽいUIを作っていて、フォームの送信ボタンをヘッダーの右上(スマホの右上)に設置したくて悩んでいて、この解決方法になんとかたどり着いた。

Reactでフォーム組むのとにかく難しくて、どこに処理書けばいいとかcontrolledかuncontrolledかとかバリデーションとかいろいろある。様々な概念とライブラリの仕様が絡み合うから厳しい気持ちになる。

jsdomを使ってテストしていたらたまに[Symbol(SameObject caches)]と言われてテストが落ちる

なんだか、とりとめもない、解決したけど結局よくわからなかったことを書いておく。これはメモ書きです。


jsdomを使ってDOMの様子を再現するテストを書いていてハマった。テストライブラリはAVAでやっているのでAVAを例に書く。

こんな感じのことを言われた

Expected promise to be rejected, but it was resolved instead

  Resolved with:

  HTMLDivElement {
    [Symbol(SameObject caches)]: {
      childNodes: NodeList {},
    },
  }

本来はDOMが見つからなくてPromiseがrejectされることを期待しているテストケースなのに、DOMがあることになっていてresolveが返ってきてしまっている

背景

AVAのテストはこういう感じにglobalにwindowオブジェクトをブチ込んでいて、

import test from 'ava';
import delay from 'delay';
import jsdom from 'jsdom';
import m from '.';

const dom = new jsdom.JSDOM();
global.window = dom.window;
global.document = dom.window.document;

require('mutationobserver-shim');

global.MutationObserver = window.MutationObserver;

test('FOO1 ?', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'foo';
        document.body.appendChild(el);
    });

    const checkEl = await m('#foo');
    t.is(checkEl.id, 'foo');
});

test('FOO2 ?', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'foo';
        document.body.appendChild(el);
    });

    const checkEl = await m('#foo');
    t.is(checkEl.id, 'foo');
});

.
.
.

似たようなDOM操作のテストケースが羅列されている状況。

どうやらエラーが起こるテストケースとは別のテストケースで生成したDOMを読んでいるのだと予想した。

実際にlogをみてみる

FOO2テストでコケていると仮定

test('FOO1 ?', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'foo';
        document.body.appendChild(el);
    });

    const checkEl = await m('#foo');
    t.is(checkEl.id, 'foo');
});

test('FOO2 ?', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'foo';
        document.body.appendChild(el);
    });

    const checkEl = await m('#foo');
    t.is(checkEl.id, 'foo');
});

FOO1で生成した<div id="foo"></div>がFOO2で見えているのではないか?と予想

FOO2でlog

test('FOO2 ?', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'foo';
        document.body.appendChild(el);
    });

        console.log(document.querySelector('#foo'))

    const checkEl = await m('#foo');
    t.is(checkEl.id, 'foo');
});

なんとnullが返ってくる! つまりFOO2テストケースではFOO1で生成したDOMは見えてない…

document.bodyをみてみる

console.log(document.querySelector('#foo'))

console.log(document.body)

すると

HTMLBodyElement {
  [Symbol(SameObject caches)]:
   { childNodes:
      NodeList {
        '0': [HTMLDivElement],
        '1': [HTMLParagraphElement],
        '2': [HTMLSpanElement],
        '3': HTMLDivElement {} } } }

なんかいる!Same Object cachesとあるとおりキャッシュっぽい??

対策

AVAのbeforeEachフックで各テスト実行時前にbodyタグ内をリセットしてやる

test.beforeEach(t => {
    // Force resetting the Object caches for each test.
    document.body.remove();
    document.documentElement.appendChild(document.createElement('body'));
});

すると

HTMLBodyElement {}

消えた。

このキャッシュなに?

developer.mozilla.org

取り除かれた子ノードはしばらくは記憶領域上に残りますが、もう DOM の一部ではありません。取り除いたノードは、oldChild オブジェクト参照を通じて、コード中で後で再利用することができます。

Node.removeChildの頁にこういうことが書かれている。

jsdomが内部的にremoveChild()してるのかはちょっとわかってないけど、もしそうならこういうキャッシュがあるらしいし、見えないけど見えるみたいなやつかもしれない。


でもまだ直らなかった

結局のところ、エラーが出ていたFOO2はAVAのt.serial()で同期処理にしてやったらよかった。実際には非同期でPromise待機しつつsetTimeoutっていうかなりややこしい関数をテストしていて、どうやらなにかしら絡み合うものがあるっぽい?AVAは普通にtest()と書くと並列テスト実行なので、どうやらそれの影響っぽかったのでserial()にしてみたら期待通りに通るようになった。

よくわからないまま解決してスッキリはしないけどメモとして書いた。

jsdomを使ってMutationObserverをテストする方法

github.com

JavaScriptでブラウザのAPIを使ったコードをテストするときはjsdomを使うと思う。大抵のDOMのAPIは揃ってるのでテストできるけどMutationObserverをテストしようとしたところjsdomはMutationObserverを持ってないのでテストできなくて困った。「MutationObserverなんてないよ」と怒られてしまう。

Polyfillを使って回避する

ようはjsdomにMutationObserverがないからMutationObserverのそれに近いものを足せばOKということになる。

github.com

ES3ブラウザに向けて書かれたshimを作っている方がいるのでこれをありがたく使わせて頂く。

$ npm install -D mutationobserver-shim

入れたらテストをこう書く。自分はAVAを使っているのでAVAの例。

import test from 'ava';
import delay from 'delay';
import jsdom from 'jsdom';
import m from '.';

const dom = new jsdom.JSDOM();
global.window = dom.window;
global.document = dom.window.document;

require('mutationobserver-shim');

global.MutationObserver = window.MutationObserver;

test('MutationObserver test', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'late';
        document.body.appendChild(el);
    });

    const checkEl = await m('#late');
    t.is(checkEl.id, 'late');
});

'm'はテストしたい自分のモジュール。mは内部でMutationObserverを利用している。

MutationObserverはブラウザではwindowオブジェクトにぶら下がっているけれどNode.jsにはそもそもwindowオブジェクトはない。ので、まずNode.jsのglobalにjsdomのwindowオブジェクトを作ってあげる。

require('mutationobserver-shim');

windowオブジェクトできたうえで、副作用requireしてMutationObserverを定義してあげる。するとこれでテストできるようになる!

めでたい

とりあえずこれでMutationObserverのテストが出来ていると思う。実際にテスト書いて通っている。

今件5年も前からIssueあるけど進捗がない。1度jsdomにPR出されてたけどパフォーマンスと実装の保守が難しいとかなんとかでリジェクトされてそのまま月日が経っている。コメントの中にAureliaのプロジェクトでTypeScriptでpolyfill書いて同じようにやっている人がいるけど、これをJS環境にもってきてもうまく動かなかった(これはたぶん自分の力量がないせいだと思う)

あくまでこの書いた方法はpolyfillを使っているので仕様策定され実際のブラウザで実装されているものとは少し挙動が違う。ので完全にテストできているとは言えないかもしれないけど、現状これしかMutationObserverのテストはできないと思う。

本来はjsdom本体に実装されて欲しいけど現状はそういうこと、ということで。

Node.jsのアップデートをするとnpmのグローバルモジュールが消える

タイトルのことで困っていた。

僕はNode.jsのパッケージマネージャにnodebrewを使っているのだけど、先日使おうと思ったノードモジュールが動かなかったのでNodeのバージョンかもしれないと思ってv8.4.0からv9.5.0に変えた。それでその時に気付いたけどタイトル通りv9.5.0にするとnpmの-gで入れていたモジュールがシェルから使えない、ということに気付いた。nodebrew use v8.4.0してv8.4.0に切り替えるとまた使えるようになる。

そういうものっぽい

github.com

これは世界ではデファクトスタンダードっぽいパッケージマネージャnvmのissueだけど、ここを見ていたらどうやらそういうものらしい。異なるnpmとnodeでは互換性が担保されないので単一のグローバルモジュールを異なるnode間で共有するのはムリが生じるからやりたくないということらしい。

自分はてっきりnpmの-gインストールはnodeと分離されて共通なものなのだという認識でいたからこれを問題だと考えてしまっていただけだった。

移行コマンドがある

github.com

nodebrewの場合は公式リポジトリのdocsのAll commandsのなかに密かに1行だけ書かれていて、上のnmvのissue読まなかったら経緯がわからないのでなんのコマンドなのかさっぱりわからないと思う。

$ nodebrew migrate-package <version>

<version>のとこは移行元のバージョン番号を入れる(v8.4.0 という形式で)

ちなみにnvmにも同様のコマンドは上のissueでもあるように用意されている。

github.com

$ nvm install node --reinstall-packages-from=node

シンボリックリンク??

それで、nodebrewで$ nodebrew migrate-package <version>したところ、なんとv8.4.0のグローバルモジュールにシンボリックリンクを貼っているっぽい。

/Users/username/.nodebrew/node/v8.4.0/lib/node_modules #にあるグローバルモジュールへ
/Users/username/.nodebrew/node/v9.5.0/lib/node_modules #にシンボリックリンク設置している

まあ当たり前に問題なく動いているのだけど、これだと以前のnodeのバージョンを消せないではないか!ということになっていてうーむ、と思っている。無限に増え続けるnode.jsは嫌なので困った。今のところはまあいいのだけど……。

nvmの--reinstall-packagesでどういう処理になるのかはわからない。nvmは使っていないので。