Edited at

TypeScript2 + webpack2でのcss-loader, file-loader周辺をなんとかする

More than 1 year has passed since last update.

WebpackとTypeScriptの扱いについて、まだ手探りだが、今のところのプラクティスをまとめたい。


css-loader, file-loaderなどで読み込むリソースが型エラーになるのをなんとかする話

webpack + babel + css-loader という構成のところからwebpack + typescriptにしていくにあたって、js, ts以外のファイルの読み込みは、当然そのままだと型がエラーとなる。

これを解決するのに、2点ほど手順が必要だった。


手順1. 読み込み方法を変更

file-loaderなどはmodule.exports = "some-resource"という形で吐き出しされるので、TypeScriptにあわせるのであれば、importの仕方を変える方が良い。

(Webpack2のtree shakingを利用する場合は、元のコードでも壊れず動いてしまうのだが、これについては後述)

// 元のコード

import style from "./foo.css"

これをこうしていく。

import * as style from "./foo.css"

// または
import style = require("./foo.css")


2. typing

型定義をしないとtypescriptは落ちるので行う。

tsconfg.jsonで独自なtypingsディレクトリを用意した上で型定義をすると良さそう。

{

...
"typeRoots" : [
"typings",
"node_modules/@types/"
]
...
}

型定義にはWildcard module declarationという定義方法を使う事で乗り越えられる。

「とりあえずanyでいいから!なんとかしたい!」という場合は、こんな具合。

// typings/resource.d.ts

declare module "*.css"
declare module "*.png"
declare module "*.jpg"

「もうちょっと毛が生えた感じに!」という場合はこのぐらいまでならサクッと定義できる

// typings/resource.d.ts

declare module "*.css" {
const classes: {[className: string]: string} // css-moduleの結果をstring型のobjectに
export = classes
// import style from "./foo.css"で読み込みたいなら下記(後述)
// export default classes
}

declare module "*.png" {
const content: string
export = content
// import style from "./some.png"で読み込みたいなら下記(後述)
// export default content
}

この定義はこのへんを参考にした。

* https://github.com/s-panferov/awesome-typescript-loader/issues/146#issuecomment-248808206

* https://github.com/Quramy/typed-css-modules/issues/2#issuecomment-256794347)

* https://github.com/Microsoft/TypeScript/issues/6615#issuecomment-188593420

「もっとちゃんと型定義したい!」という場合は、下記などを導入するのが良いだろう。


webpackでのimport解決のされ方はtsconfig.jsonのcompilerOptions.moduleによって変わるという話

TL;DR

* module: es2015の場合、typescriptはモジュール解決に関与しない。webpackにモジュール解決が移譲されて、babelっぽく扱われる(babelと互換性が維持されるが、ESmoduleの仕様とズレるので、今後どこかで変更されるだろうと思われる)

* module: commonjsだと、typescriptがimport / exportを変換する。互換性は維持されない(正しいESmoduleの仕様に準拠する。babelとの互換性はなくなる)


前提(と疑問)

例えばurl-loaderfile-loaderは、下記のような吐き出しをする

/******/ ([

/* 0 */
/***/ (function(module, exports) {

module.exports = "data:text/plain;base64,aGVsbG8="

/***/ }),

ESmoduleの読み方で言えばimport * as foo from "foo"という読み方でないと読み込めない理屈になる。

しかし、自分の手元で正しく動いていたので、「これは何故だろう?」という疑問が出てきた。


実験

まずこんなTypeScriptファイルを用意

import * as txt1 from "./foo.txt"

import txt2 from "./foo.txt"

console.log("===========")
console.log("import * as txt1")
console.log("=>", txt1)
console.log("===========")
console.log("===========")
console.log("import txt2")
console.log("=>", txt2)
console.log("===========")

Webpack設定を用意。

今回は、外からcompilerOptions.moduleを変更できるようにする。

module.exports = ( env ) => {

const { moduleType } = env
return {
entry: './src/index.ts',
output: {
path: `output/${moduleType}`,
filename: 'bundle.js'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{ test: /\.txt$/, use: 'url-loader' },
{ test: /\.ts$/, use: [{
loader: 'ts-loader',
options: {
compilerOptions: {
// tsconfig.jsonのmoduleを上書き
module: moduleType
}
}
}]},
]
}
}
}

そしてビルド→実行をする

まずcommonjsの場合

$ webpack --env.moduleType=commonjs

$ node output/commonjs/bundle.js
===========
import * as txt1
=> data:text/plain;base64,aGVsbG8=
===========
===========
import txt2
=> undefined
===========

結果:import txt2 from "./foo.txt"undefinedになる。

次にes2015

$ webpack --env.moduleType=es2015

$ node output/es2015/bundle.js
===========
import * as txt1
=> data:text/plain;base64,aGVsbG8=
===========
===========
import txt2
=> data:text/plain;base64,aGVsbG8=
===========

結果:import * as txt1 from "./foo.txt"import txt2 from "./foo.txt"、どちらもstringが取得できる。


解説


commonjsの場合の話

Typescript(ts-loader)の段階で、import module from "./module"は下記のような感じで変換される

var module_1 = require("./module");

var a = function () {
module_1["default"]();
};

最終的にこんな感じに変換される。

var txt1 = __webpack_require__(0);

var foo_txt_1 = __webpack_require__(0); // txt2として読み込んだもの
console.log("===========");
console.log("import * as txt1");
console.log("=>", txt1);
console.log("===========");
console.log("===========");
console.log("import txt2");
console.log("=>", foo_txt_1.default); // .defaultにアクセスしてる
console.log("===========");

file-loaderはmodule.exports.defaultに何も吐き出してないので、commonjsモードでimport foo from "./foo"と読み込むと、undefinedになってしまう。


es2015の場合の話

TypeScriptはimport / export構文をいじらず、そのまま吐き出す。

import mod from "./module"

const a = () => {
mod()
}

これをwebpackが受け取り処理する。

webpackのloaderはbabel同様、__esModuleフラグを利用した後方互換維持の処理をしている。(これを ESM interopと呼んでるそうだ)

source

/******/    // getDefaultExport function for compatibility with non-harmony modules

/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };

最終的にはこんな感じになっている。

"use strict";

Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo_txt__ = __webpack_require__(0);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo_txt___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__foo_txt__);

console.log("===========");
console.log("import * as txt1");
console.log("=>", __WEBPACK_IMPORTED_MODULE_0__foo_txt__);
console.log("===========");
console.log("===========");
console.log("import txt2");
console.log("=>", __WEBPACK_IMPORTED_MODULE_0__foo_txt___default.a); // .defaultへのアクセスではなく、Object.definePropertyのgetterとしてアクセスするように変換されている
console.log("===========");

これによって、結果的に`import foo from "foo.css"でも読み出せるようになってる模様。

この後方互換は今後どうするかは議論対象になっている様なので、ある程度注意はしたほうが良さそう(とはいえ、議論を軽く追ってみるとwebpack2の間に突然消える、のようなことが起きることはなさそうに思える)

Webpack2(babel)の仕様に併せてimportが壊れるケースもあるらしいので注意


まとめ


  • webpackのtree shakingな読み方はbabelと同じ事やってるので、es2015にすればloader系がいきなりぶっ壊れたりはしない。

  • 可能なら、型によって読み出しを制御しつつ、TypeScriptの手法に併せていった方が無難と思われる。

  • 「どうせloader使ってるならwebpackに併せてしまえばいいのでは?」という判断とかもありえるが、そこらへんは自己判断で。

  • commonjsにして変な読み出しをさせないようにする、というのも無くはなさそうだが、型で防ぐでも十分には思える

  • webpackもそのうちこの辺の読み出しに動きがあると思われるので、何処かでは書き換えが発生するものと考えたほうが良いだろう。