Node.jsのES Modulesサポートの現状確認と備え

この話を今日のNode学園で話すので、ご興味あればどうぞ。

nodejs.connpass.com

(今日いくつか加筆修正しました)


ECMAScript 2015で待望のES Modules(ESM)の仕様が策定されたものの実装がなかなか進まない、という話を1年前に発表した

その後、ブラウザでのES Modules仕様が策定完了し、フラグ付きながら全主要モダンブラウザで初期実装が揃った (caniuse)。(dynamitterさんkijtraさんからのコメントを受けて修正)

そしてついに、揉めに揉めていたNode.jsのES Modulesネイティブサポートにも7月に入って動きがあり、draftというステータスながらも仕様がマージされ、初期実装のプルリクが投げられた。

ということで、現在進められているNode.jsのES Modules仕様と実装を眺めてみたので紹介する。


実はのんびり書いている間に下記のブログが公開されました。

重複部分は省略してるので、上の記事と前回のスライドを合わせてご参照ください。

背景: なぜそんなに揉めていたの?

ES2015の言語仕様では、ES Modulesのシンタックスは決めたが、それが実際どう動くかはホスト側が決めることになっていた。 つまり、ブラウザであればWHATWG, サーバーであればNode.jsの各陣営が個別にES Modulesの周辺仕様を策定していく必要があるため、実装方針だけ決めれば良い他の機能に比べて時間がかかっていた。

特に論点になったのは以下3点。

  • そのファイルがES Modulesかどうかを判定する方法
  • 識別子の解決方法
  • 既存のCommonJS Modulesとの相互互換性

3点目について、ブラウザとは違って、既にCommonJS Modules (以下CJS Modulesまたは単にCJS) というモジュール機構を持っているNode.jsでは相互互換性という面倒な要素を考える必要があった。

結果的にブラウザとNode.jsでは以下のようになった

Browser Node.js
ES Modules判定 <script type="module"> .mjs 拡張子
識別子の解決 URL (絶対 or 相対) URL (Node.js方式の探索)
CJS Modules互換性 N/A 制限付きで互換

ブラウザについては前述のスライドや記事を参照してもらうとして、以下ではNode.jsについて見ていく。

TL;DR: 今どうすればいいの?

長くなるので、つべこべ言わずに今どんなコードを書けば良いのか教えて!という人のために、先に今押さえておくべきことだけピックアップ。

安定派はまだCommonJS Modulesを

多くの人にとって、まだ仕様が不安定なESMでコードを今書くメリットは「かっこいい」「キマル」ということ以外にほとんどない。 世間に踊らされずrequire()を書いていれば、あとでNode.jsにESMが来たときでも互換性が確保される予定なので、これまで書いたパッケージをESMからimportできる。安心。

それでもES Modulesを書きたい人は

以下2点に気をつける。

  • ESMからCJSをimportする場合、 named importを使わない。default importだけを使う。
  • ESMの中でrequire, module, __dirname, __filenameは利用できなくなるので、できるだけ使わない、または後で変更する覚悟をしておく。

依然として議論が分かれている.mjs関連に比べて、この辺りはコンセンサスが取れているようなのでそのまま通りそう。今から気をつけておくと修正リスクが減らせる。

特に1点目については、現状のBabelを使ったES Modulesもどき (Babel Modulesと仮称) の世界では普通に利用されているので要注意。 つまりCJSをこのようにnamed importするコードは、Babelでは動くがネイティブなES ModulesがサポートされたNode.jsではエラーになる。

import {readFile} from 'fs';
// SyntaxError: The requested module does not provide an export named 'readFile'

以下のようにdefault importで書き直す必要がある。

import fs from 'fs';
fs.readFile(...);
// or 
import fs from 'fs';
const {readFile} = fs;
readFile(...);

拡張子.mjsについては議論も実装もまだ揺れているし、機械的にリネームするのも難しくないので、今はとりあえずスルーしておくのが良さそう。

ではここから詳細の話。

ES Modulesの判定方法: .mjs

現行の仕様では、Node.jsにファイルをES Modulesとして認識させるには、ファイル拡張子を.mjsにする必要がある。 .jsファイルは従来のCJS扱いになるため、.jsの中でimportexportを使うとシンタックスエラーになる。

.mjsは新しい拡張子のため、ローカル開発環境のエディタ、IDE、ビルドツールやlintツールなどの設定変更が必要になる。

一方、ブラウザは拡張子は全くチェックしない。その代わりcontent-typeヘッダがJavaScriptとして適切かはチェックする(<script type=module>はデフォルトでX-Content-Type-Options: nosniffと同等にcontent-typeをチェックすることになったため、application/octet-stream等ではJavaScriptとして実行してくれない)。よって、.mjsファイルをブラウザでもJSとして実行させたい場合、配信時にcontent-typeヘッダをapplication/javascriptなどJavaScriptとして適切なものにする必要がある。そのために、例えばnginx/apacheなどのサーバー設定で.mjsファイルに対する設定を変更しなければいけない。

識別子の解決: URL (Node.js方式で探索)

ES Modulesの識別子(下記でいう'./foo'の部分)をどう扱うかについて。

import foo from './foo';

まず、ブラウザでのES Modules仕様では識別子はURL (WHATWG URL) として扱われる。 Node.jsのESMでもこれを追随して、URLとして扱うことになった。 そのためURLで特殊扱いされる区切り文字、例えば:,?,#, %などの扱いが従来のCJSの識別子とは異なる。 通常の用途ではまあ問題ないと思う。(詳細はHTML Standard | 8.1.3.8 Integration with the JavaScript module systemを参照)

探索方法は基本的には旧来のNode.jsの方式を継承している。従来と異なるのは、自動で付与される拡張子に.mjsが追加されていること。優先順位は.jsより高い。

  1. 識別子がfile://から始まる絶対URLである場合、そのままURLとして扱う
  2. 識別子が/, ./, ../で始まる場合、それを相対URLとして扱ってパスサーチを行う
    1. 識別子をファイル名として探索
    2. 拡張子を順に付与して探索
      • [".mjs", ".js", ".json", ".node"]
    3. ディレクトリとして探索
      1. ${specifier}/package.jsonmainを読む
      2. ${specifier}/indexを読む
  3. 識別子がそれ以外の場合、モジュールサーチを行う
    • ./node_modules/${specifier}を親ディレクトリをさかのぼって探索

つまり、

import foo from './foo';

と書かれた場合、以下の優先順位になる。

  1. ./foo
  2. ./foo.mjs
  3. ./foo.js
  4. ./foo.json
  5. ./foo.node
  6. ./foo/package.jsonmain を探索
  7. ./foo/index(拡張子を変えて探索)

「6. package.jsonの探索」では、例えばmain./barだった場合、

  1. ./foo/bar
  2. ./foo/bar.mjs
  3. ./foo/bar.js
  4. ./foo/bar.json
  5. ./foo/bar.node
  6. ./foo/bar/index(拡張子を変えて探索)

のように探索される。これを利用して、npmパッケージをESM/CJS両対応でリリースすることができる(後述)。

これを踏まえて、Node.jsでESMからESMを呼び出すのは以下のようになる。(普通です)

// foo.mjs
export default 'foo!';
// main.mjs
import foo from './foo';
console.log(foo); // 'foo!'

上述の通り、識別子の拡張子は自動で補われるため省略して./fooと書ける。

しかし、このままブラウザに配信しても動作しない。拡張子自動付与はNode.js独自の仕様であるため。 変更/変換なしでそのままブラウザでも動かしたい場合、以下のように自分で拡張子を書いてあげる必要がある。

// main.mjs
import foo from './foo.mjs';

ただ、この書き方をすると後述するESM/CJS両対応パッケージを作りにくくなるのが悩ましいところ。

CJS Modulesとの互換性

ES Modulesで使えなくなる変数たち

まずECMAScriptレベルで、ES Modulesでは従来のscriptに比べて以下のような違いがある。

  • 強制的にStrictモードに
  • thisundefined
  • global scopeではなくtop level scopeに
  • top level await予約語

それに加えて、Node.jsのES Modulesでは、従来のCJSでは使えた以下のようなグローバル変数が削除されている。

  • require
  • module
  • exports
  • __filename
  • __dirname
  • arguments

最初の3つが意味することは、ESMの中でrequire()を使ってCJSを読み込んだり、ESMとCJSの両方の方式でexportしたりすることはできないということ(CJSを呼び出す方法は後述)。

__filename, __dirnameについては、URLベースになったため単純な置き換えが不可能として削除された。将来的にはES proposal import.meta (現在Stage 2) によってファイルのURLが取得可能にすることが検討されている。詳細は未定だが、こんな感じになりそう。

const url = import.meta.url;
// file:///foo/bar.mjs

import.metaは、言語仕様に入らないホスト依存のメタ情報をモジュール内部に渡す役割をする特殊なプロパティ。他にはブラウザではモジュール内部からscript要素の各種属性へのアクセスなんかもこれを使うことが検討されている。

この仕様が間に合わない場合、__dirnameについては同ディレクトリのCJSからimportするというwork aroundはある。

// expose.js
module.exports = {__dirname};
// use.mjs
import expose from './expose';
const {__dirname} = expose;

ES ModulesからCJS Modulesを読み込む

ESMからは、CJSを直接importで読み込むことができる。

// cjs.js
module.exports = function two() {
  return 2;
};
// es.mjs
import foo from './cjs';
// foo = two;
foo(); // 2

import * as bar from './cjs';
// bar = {default: two};
bar.default(); // 2
bar(); // throws, bar is not a function

CJSはimportされると、評価されたあとに{default: module.exports}のようなESMとして扱われる。

注意点としては、CJSがESMに変換されるとmodule.exportsのオブジェクトがそのままdefault exportになるため、先述した通りnamed exportは利用できない。繰り返しになるが、以下のように書くとエラーになる。

import {readFile} from 'fs';
// SyntaxError: The requested module does not provide an export named 'readFile'

つまり、インポート先のパッケージがESMなのかCJSなのか把握しておく必要がある。

従来のrequireように、宣言的ではなく命令的に、かつ同期的にCJSを読み込みたい場合はどうするの?という要望に対しては、import.meta.requireとして提供するということも検討されているようだ。

CJS ModulesからES Modulesを読み込む

前提として、CJSからESMを読み込むには、ES proposal Dynamic Import (現在Stage 3) を使う必要がある。 つまり、将来Dynamic Importが実装されたNode.jsのバージョン(現時点ではまだV8に実装中 (ついさっき実装完了した模様)でだけ使えるもの。 現行のNode 8等で実行したCJSファイルからESMを読み込めるというわけではない(もちろんpolyfill等は出てくるだろうけど)。

Dynamic Import

import()はPromiseを返すので、そのまま.then()でつなぐか、awaitで受ける。

// es.mjs
let foo = {bar:'my-default'};
export default foo;
export function f() {};
export class c {};
// cjs.js
(async () => {
const es_namespace = await import('./es');
// es_namespace ~= {
//   get default() {
//     return result_from_evaluating_foo;
//   }
//   get f() {return f;}
//   get c() {return c;}
// }
console.log(es_namespace.default);
// {bar:'my-default'}
})();

Dynamic Import自体は、CJSとESMのどちらの中でも使える。ESMでは条件別ロードや遅延ロードで利用される想定。

注意点として、前述した通りDynamic Importではnamed importしか扱えない。default importに対するshort handは用意されていないため、上述のように.defaultにプロパティアクセスするか、以下のようにDestructuringを使うしかない。

const {default as bar} = await import('./es');
console.log(bar);
// {bar:'my-default'}

ESM/CJS両対応パッケージの作り方

前述のように、package.jsonmainプロパティで拡張子を省略すると、

{
  "name": "esm-cjs-compat-package",
  "version": "1.0.0",
  "main": "index",
}
  • importされた場合: index.mjs
  • require()された場合: index.js

という挙動になるので、Babel等でimport/export文をCJSにトランスパイルすると同時に拡張子を.mjsから.jsに変換し、両方をnpmパッケージにバンドルすると、ESM/CJS両対応パッケージとなる。

サンプルリポジトリを用意したので詳細はこちらを参照。 https://github.com/teppeis-sandbox/esm-cjs-compat-package

FAQ

なんで.mjsなの?

ESM判定方法について、.mjsの対抗案は以下2つ

どれもpros/cons、トレードオフがあって、完璧な解決策はない。 module関連のissueはbikeshedなため数百のレスがついてしまいもはやオープンな議論はほぼ不可能なんじゃないかという印象なので、個人的にはとにかく最初の仕様と実装を進めることが大事だと思う。その最初の実装として.mjsは仕様も実装もシンプルなので進めやすい。 ES Modulesの議論をリードしているbmeckも、.mjs以外の案について排他ではなくadditionalな仕様として検討したいと発言している。 そうじゃないと、正式なES Modulesが出る前にBabel Modulesがどんどん広がって本当に収拾がつかなくなってしまうので。

ちなみに"use module";については、先月のTC39でECMAScript仕様に入れるか議論されたが、ネガティブな意見が多く進まなかった。ES仕様に入れるためにはNodeだけでなくブラウザでも有益であることを示すことが求められた。ES仕様に入れなくてもNodeの独自仕様として"use module";を採用することもできる(ECMAScript仕様に準拠した拡張として可能)が、Node陣営としてはそれは行わない方針らしいので、このまま行くとこの提案はお蔵入りになりそう。

.mjsの不安なところは、マネージドサービスのcontent-typeが対応してくれるかどうか。 例えば現状でGitHubは対応していないため、GitHub Pagesから.mjsファイルをscript要素で読み込んでも実行できずエラーになる。 .mjsというNode.js特有仕様なので(Web/ブラウザの仕様ではない)、それを各種サービスのWebサーバー設定にどれだけ反映してくれるのだろうか。

現状、IANAへのmedia type登録や、nginx, apacheでのデフォルト設定の改善などに向けて動いているとのこと。h2oにもプルリクが来ている。VS Codeの対応はマージ済み

NodeのCLIで標準入力や-eから実行するときはどうするの?

拡張子を指定するためのフラグが検討中。

$ node --entry-extension=mjs < app.mjs

github.com

なんで__filenameなどのグローバル変数が削除されたの?

CJS Modulesでは以下の様なモジュールラッパーによって特殊な変数を提供していた。

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

https://nodejs.org/api/modules.html#modules_the_module_wrapper

ESMの提供というタイミングで、識別子がURLベースになったこと、非標準なグローバル変数を外したい、といった要因で削除された

今後、こういったホストから提供される情報はimport.metaを経由するのが標準になっていくはず。その方がbrowserify的な変換もしやすいし。

いつ使えるようになるの?

現時点でプルリクはまだレビュー中。大きな問題があるというより細かな修正がメインをしながらレビュアーも徐々にES Modulesを学んでいるという印象。

プルリクでは--harmony-modulesフラグ付きで実行することになっている(フラグの名称は議論中なので変わるかも)。 ついさっき--experimental-modulesに変更された。 Node v9のtracking issueによると、v9は9月中にfeature freezeしてbetaリリース、10月にRCをリリース予定としている。 おそらくこのNode v9でES Modulesのフラグ付き実装がリリースされそう。 かなり影響が大きい変更なので、次期LTS Node v10でフラグを外すのは難しいと予想する。 順調に行ってもNode v11でフラグを外してNode v12でLTSデビューという流れか。

今、Node.js版ES Modulesの仕様を試す方法は?

node-epsのプルリクのブランチをビルドするか、ちょうど先日 @std/esm というポリフィルパッケージがリリースされたので試せる。

medium.com

ランタイムでacornでパースしてESMとして各種AST変換やdynamic importのポリフィルを当てるライブラリ。 パフォーマンスへの配慮か、tar.gzで固めるなどパッケージングの仕方もおもしろい。

コンパイル無しでUniversal JavaScriptは実現しますか?

識別子の仕様の違いから、素の静的なHTTPサーバーだけで実現することは難しい。

  • 識別子の拡張子を省略するとブラウザは解釈できない。
  • node_modules以下のnpmパッケージを参照する方法がない。

1点目は、import from './foo'では動かないので./foo.mjsにする必要があるが、そうするとNode.js側でのフォールバックが素直にはできなくなって辛い。

2点目は、ブラウザの仕様ではimport from 'foo'のような、絶対URLでもなく、.または/から始まらない文字列は認めていないのでエラーになる。

いずれの問題も、現状では、webpack的なものでプリコンパイルするか、ランタイムで解決する専用アプリサーバーで配信するか、ブラウザ側でService Workerで変換を挟むなどして頑張る必要がある。

Babelは分かったが、TypeScriptはどうなの?

まだちゃんと議論は始まってないけど、おそらくちゃんとコードを書いてればTypeScript側のトランスパイルで吸収してくれそう。 TypeScriptには二つのモジュール構文がある。DefinitelyTypedなどの型付では、元のソースがCJS Modulesの場合は旧形式 import/require、ESMに対しては新形式 import/from を使うというルールがある(と思う)。なので、それが徹底されている限りはTypeScriptのトランスパイル側で吸収してくれるはず。

この辺りは、型の整合性を保つ必要があるので、Babelに比べると厳格に検討されていたことが結果的に良かった。(逆に二つのシンタックスを使い分ける必要があったり、defaultの扱いがBabelに比べると面倒だったりするのだけど)

thank-you-starsで利用しているnpmパッケージに気軽にスターを送る

先週、こういうツイートを見て、

共感したのでサクッと作った。

github.com

package.jsonと同じディレクトリで実行するだけで、depsとdevDepsのパッケージのGitHubリポジトリにスターできる。 事前にパーソナルトークンをホームディレクトリに保存しておく必要があるけど、その辺はREADMEを読んでくれ。

依存に入れて使っているということは、それなりに恩恵を受けているということなので、問答無用でスターを送ってしまって良いと思う。 孫依存のパッケージにも送るか迷ったけど、npm的にそこ含めると一気に数が膨れてしまうのでやめた。

これでみんなももりもりスターを送ろう!


package.jsonに関する知見

これだけの単純なパッケージながら多少の知見が得られたので共有。

package.jsonの正規化はnormalize-package-data

package.jsonのrepositoryプロパティは、正規形は

{type: "git", url: "git@github.com:facebook/react.git"}

のようなオブジェクト型だけど、文字列型でfacebook/reactなどの特殊形も認められていて扱いがめんど臭い。 npmの中で使っているパッケージを調べたらこれだったので同じものを使った。

これによって、typeとurlのあるオブジェクト型に統一される。他のプロパティも一通り正規化できるし、オプションによってバリデートも可能なので、package.jsonを扱うときに必要なら使うと良い。

Git repositoryの情報抽出はhosted-git-info

repositoryプロパティがオブジェクト型に統一されても、gitのURLにはssh, git, httpsというプロトコルの種類だけでなく、npmが認めているGitHub, Gist, BitBucket, GitLabなどの特殊な省略形式があって、これまた扱いが面倒。npmの内部ではこれを使っている。

これによって、URLをパースしてサービス、ドメイン、ユーザー、プロジェクトを抽出するだけでなく、各種プロトコル用のURLやinfo.bugs()GitHub IssuesのURLを生成できたりして、非常に便利。package.json以外の用途でもGitHubいじるときに使えそう。

CircleCI 2.0 GAリリース: 続Node.jsマルチバージョンビルド

CircleCI 2.0 GA が無事リリースされたので、betaが取れるのを待ってた方は是非どうぞ。 今の所は爆速だったbeta時代と遜色ない速度で動いてる。

circleci.com

1.0やTravis CIと比較してのCircleCI 2.0のメリットは前回記事に書いたのでご参照ください。

CircleCI 2.0 でNode.jsのマルチバージョンビルド - teppeis blog

続きを読む

CircleCI 2.0 でNode.jsのマルチバージョンビルド

追記 2017/07/11

CircleCI 2.0 GAがリリースされたので、curlではなく標準機能Workflowsを使ったバージョンを紹介する記事を書いたので、こちらをご参照ください。

CircleCI 2.0 GAリリース: 続Node.jsマルチバージョンビルド - teppeis blog

追記終わり。


CircleCI 2.0が高速でカスタマイズできて最高だという話と、Node.jsのマルチバージョンテストのやり方、キャッシュ戦略などを紹介する。

前提として、以下はnpmパッケージのようなユニットテストでほぼ完結するシンプルなライブラリのCIを想定している。 サービスやアプリ開発のCIは要求が違ってくるのでまた別。

先に結論としてサンプルプロジェクトを貼っておく。急ぎの方は.circleci/config.ymlをコピペして “How to use” だけ読んで設定したらOK

github.com

CircleCI 2.0はまだβテスト中なので、もろもろ変更される可能性はある。 ここから申し込めばすぐ使えるようになるのでGO。

続きを読む

npm install scriptの脆弱性とオープンソースと信頼

先日アナウンスされた脆弱性とその周辺について、とりとめなく。

脆弱性の概要

VU#319816 によれば、今回問題になっているのはnpmの以下の性質を利用するとnpmパッケージでワーム(自己増殖力のあるマルウェア)を作れるというもの。

  • 依存パッケージのバージョンをロックせず、semverにより範囲指定することが多い
  • CLIで一度npmへloginすると、明示的にnpm logoutするまで認証が永続化される
  • npm registry が中央集権型サーバーである

具体的な手法として、Chris Contoliniが PoC として pizza-party というリポジトリを公開している*1。以下のように動作する。

  1. ワームが仕込まれたパッケージを依存関係に含むパッケージを npm install する
  2. ワームの実体であるinstallスクリプトが実行される
  3. ワームは親のnode_modulesにある全てのパッケージに対して、
    1. package.jsonのinstallスクリプトにワームを仕込む(このような
    2. npm version patchでパッチバージョン番号を上げる
    3. npm publishでワーム入りパッケージとしてアップデートを公開

Chris Contoliniのブログによれば、99.99%はそのパッケージのオーナーではないのでnpm publishが失敗するだろうが、npmの外部パッケージ依存度の高さから一部のオーナーに感染すれば雪だるま式に拡散するだろう、と主張している。

IMO: ローカルのnode_modulesを探索するより、npm whoamiからユーザー名を取得して npmjs.com から所有パッケージを根こそぎダウンロードしてワーム仕込んだ方が効率的な気がする。

何が問題か?

installスクリプトに攻撃を仕込まれる危険性については、まあある意味当然というか仕様だし、1年前に rimrafall という、installスクリプトrm -rf /*するPoCパッケージが公開された騒動でも話題になった。これに対してnpm inc.は問題あるパッケージを都度削除する対応を取った。

今回のポイントは、ワームが仕込まれてても同じことできるの?ということ。有名パッケージも含めて大量のパッケージがガシガシ感染していったらどうする?都度削除とかムリだし取り返しつかない状態になるのでは?と。

たしかに、substackやsindresorhusが感染したらと想像するとやばそう。

npmブログの見解としては、publishの頻度をモニタリングしてるから、ワームが広がり始めたら全体のpublishを止めるよ、と言っている。

どうしたらいいの?

さて各ユーザーはどう対応したら良いのか。

--ignore-scripts

npmはブログでは以下の方法が紹介している。

  • npm install--ignore-scriptsオプションを付けてスクリプトを実行させない
  • npm config set ignore-scripts trueで上記動作をデフォルトに設定する

しかし、npm自身もブログで書いていることであるけど、installスクリプトを防いだところで、そのパッケージ自身に悪意のコードがあればrequireした時点で攻撃されてしまう。感染力を弱める効果は多少あるかもしれないけど、攻撃コードの実行を防ぐという意味では正直気休め程度かなと思う*2

installスクリプトが動かないことでインストールできないパッケージもあったりして面倒(行儀悪いけど)。

npm shrinkwrap

shrinkwrapを使って子孫すべての依存関係をロックして、感染したパッケージがアップデートしたバージョンの混入を防ぐ方法。

依存関係のアップデートするときどうするの?というのはあるけど、感染力はだいぶ弱まりそう。 ただ、ライブラリ系だと現実的には運用が厳しいし、全パッケージがshrinkwrapし始めたらnpmの思想を根本的には壊しかねない気がする。

npm logout

こまめにnpm logoutして、npm publishなど必要なときだけログインする方法。

ログアウトしておけば、自分自身の環境での悪意のコード実行は防げないけど、自分が所有するパッケージへの感染による拡散は防げる。認証が必要な操作って普通はpublishぐらいなので、普通の人は頻繁に使わないから問題なさそう。

npmさん、ぜひログイン状態を永続化せず都度パスワードを聞くようにするオプションを付けて欲しい。

npmブログによれば、2-factor authを準備中らしい。

オープンソースと信頼

ということで、今回のワームの拡散は認証周りをちゃんとやれば防げそうそうだけど、根本的な問題である悪意のあるコードの実行を防ぐのは難しい。

npmブログから引用すると、

You should not execute any software downloaded from the Internet if you do not trust it, including software downloaded from npm.

The npm Blog — Package install scripts vulnerability

「インターネットからダウンロードした信頼できないソフトウェアを実行すべきではない、npmも含めて

当たり前ではあるのだけど、当のnpmに改めて言われるとなかなか重い。

また、Chris Contoliniのブログにはこう書いてある。

Using other people’s code is inherently risky

Building an npm worm - Chris Contolini

普段は目をそらしがちではあるのだけど、オープンソースだからといってセキュアなわけではないし、誰かが見てくれている保証もない。

こういう問題ってモダン≒オープンソースをベースにしたソフトウェア開発では共通する問題だと思うけど、多くの細かい外部パッケージに依存しがちなNode.js世界では特に大きな影響がある。

ちょうど先日のkik/left-pad問題にもつながる話だ。

マイクロパッケージ文化には光の側面闇の側面がある。

自分が依存するパッケージ、その子孫までの中にどこにも悪意のコードが無いことを自信を持って言えるだろうか?ねずみ算式に増えるので自分ですべてチェックするのは現実的にはムリだろう。

ただの考えすぎで善意が解決してくれる問題なのか、プロジェクトの性質によって対応を考えればよいのか。他の言語では何か仕組みがあるのかな?できればnpmの自由な雰囲気と両立できる何かがほしい。

よーしわかった、セキュアな大統一汎用標準ライブラリがあればいいんだ!あ、そうだ、Closure Library っていうちょうど良いものがあるじゃないですか!なんだ、すべては杞憂だったのか(違う)。

*1:npmには公開されてない。取扱注意

*2:例えばmainスクリプトの冒頭に埋め込まれたら?

ES2016の追加機能が決定、あるいはES7言うな問題

今年6月に公開予定の ECMAScript 2016 (ES7) で追加される機能が以下の2つに決定した。

今後は3月1日にスペックのスナップショットが切られ、6月の公開に向けた作業が始まる

あれ、ES7ってそれだけなの?と思った方は、ぜひ ECMAScript の新しい策定プロセスを確認されたし。

標準化には2つの実装が要求される

昨年公開されたES6以降、ECMAScript の仕様策定プロセスは機能ベースで毎年リリースするスタイルになった。

細かいプロセスは 『ECMAScriptの仕様策定に関するカンニングペーパー | Web Scratch』 を参照してもらうとして、重要なポイントは、提案機能が標準化される(Stage 4になる)ためには2つ以上の実装が必要ということ。

今回のES2016については、1月26-28日にSans Franciscoで開かれたTC39 Meetingをもって実装の締め切り宣言がなされた。

Exponentiation Operator の実装状況は、EdgeがPreview版にフラグ付きで実装済みSpiderMonkey (Firefox Nightly) では実装済み、という状況。これで実装が2つとカウントされたことから、少なくとも4大ブラウザに実装されればフラグ付きだったりStable版でなかったりしてもカウントされると考えて良さそう。V8には実装されたけどChromeには載ってない、とかだとどうなんだろう。

ちなみに Async/Await は Edge Preview にフラグ付きで実装されただけだったので、実装2つには足りず ES2017 に持ち越された。

ES7 言うな問題

ES2016 (ES7) が確定したので、改めて。

Firefox Dev Confのスライド にも書いたのだけど、ECMAScriptの新しい仕様策定プロセスの不理解や過去の経緯から "ES7 Decorator" や "ES7 Async/Await" という言葉が使われがちという問題があった。いよいよES7の機能セットが確定して明らかな誤用と言えるレベルになってきたので、さすがにもう使うべきでない。

Stage 3以下の提案中の仕様については、ES.nextとかES proposalsと言及するか、あるいは単にESと言う方がまだ誤解が少ない。

domenicも言っているようにECMAScript においてはもはやバージョン番号は重要ではないのだから。

追記

ときを同じくして Dr. Axel 先生も同じようなエントリを書いてた。

If you are disappointed that your favorite stage 3 feature did not make it into ES2016 – don’t worry: With the new release process, it’s more about the stage a proposal is in than what release it is a part of. As soon as a proposal reaches stage 4, it is done and safe to use.

The final feature set of ECMAScript 2016 (ES7)

書かれてる通り、ある機能が ES2016 に入るか入らないかはあまり重要ではない。その機能が Stage 4 に上がる(実装が2つ揃う)のがいつなのかが重要なのであって、その時点が含まれるのが ES2016 か ES2017 なのかは年度の切り替えがいつかという手続き上の問題に過ぎないので。

追追記

Jay Phelpsがほぼ同じ記事をポストしている。

彼はES.nextよりもさらに誤解の少なくなるよう、"stage-1 decorators" のように "stage-x" を付けて呼ぶか、または単に "proposed features" と呼ぶことを推奨している。

npm initでauthorやlicenseなどの初期値を指定する

JavaScript Advent Calendar 2015の2日目。小ネタです。

npm initするときにauthorとかlicenseとか毎回同じこと入力するの面倒だったりしません?実はいくつかの項目は初期値を設定できるのだけど、ググってもあまり日本語情報が無いようなので共有。

設定方法は.npmrcで以下のように指定すればOK。

init-author-name=Teppei Sato
init-author-email=teppeis@gmail.com

.npmrcの場所は以下の順で優先される。(詳細は公式を参照)

  1. per-project config file (/path/to/my/project/.npmrc)
  2. per-user config file (~/.npmrc)
  3. global config file ($PREFIX/etc/npmrc)
  4. npm builtin config file (/path/to/npm/npmrc)

今回は全プロジェクトに指定したいケースなので、ホームディレクトリの~/.npmrcに書いておくのが良いと思う。

指定できる設定値は公式docを参照。init-で始まる項目をリストアップしてみる。

init-author-name, init-author-email

名前とメールアドレス。これはたぶんみんな便利になるはず。

init-author-url

自分はURL書いてないので使ってない。

init-license

デフォルトのライセンスはISCなので、自分はMITを初期値に指定してる。

init-version

デフォルトが1.0.0の強気設定なので、0.0.1から始めたい弱気な人はどうぞ。

init-module

これを指定するとnpm initが呼ばれたときにロードするJSファイルを置き換えることができる。よりカスタマイズしたい上級者向け。

完全に置き換わるので、いつもの対話的にパッケージ名等を聞かれる処理はいっさいなくなっちゃうから必要なら自分でゼロから書かなければいけない。実はinit-moduleの初期値は~/.npm-init.jsになっていて、ファイルがあれば発動するようになっているので、init-module自体を設定しなくてもファイルを置けば利用可能。

npm initの実体は npm/init-package-json パッケージで、そのうち default-input.js がデフォルトの対話式スクリプトの実体なので、これをベースに改造するのが良いかも。


以上です。

あとnpm initとは関係ないnpmrcの小ネタとして、tag-version-prefix を変更するとnpm versionで"v1.0.1"になるのを"1.0.1"にできるようになるよ。

Enjoy happy npm life!