定数は DefinePlugin を使ってどこからでも参照できるようにすればいいと思う

テーマの管理辺りとか特にそう思います。例えばそれは、constants/theme.tsなどにオブジェクトで定義して、それを使うコンポーネントすべてでインポートして使ったりしたりといった具合にです。ただコンポーネントファイルはそれこそ数百ファイル以上になることは普通なので、いくらアウトインポートができるからと言っても流石に効率悪すぎなのではと感じています。

DefinePlugin でオブジェクトそのまま渡す

そこで例えば以下のような Webpack 設定があったとします。このように書いてしまえばコードの中ではインポートは書かずとも、COLOR.accentCOLOR.textと書くだけでそれら定数が使えます。

const webpack = require('webpack');

module.exports = {
  /* 色々 */
  plugins: [
    new webpack.DefinePlugin({
      COLOR: JSON.stringify({
        accent: 'orange',
        text: '#222',
      })
    }),
  ]
}

ちゃんと型定義をする

入っているか分からなくなりそうですが、それはtypes/define/index.d.tsなどに以下のように書きます。これで型も効くし、サジェストも効くので TypeScript に慣れた人には問題もないと思います。ここはアナログになり少し面倒くさいかもしれないですがインポートの方がもっと面倒くさいかなと個人的には思います。

declare const COLOR: {
  accent: string;
  text: string;
}

EnvironmentPlugin で環境変数をコード内に渡す

NodeJSでは、環境変数はprocess.envというオブジェクトの中に入っていますが、この習慣をクライアント側でも使おうということでprocess.env.xxxを定義して使えるようにするのが EnvironmentPlugin の役割です。この EnvironmentPluginwebpackパッケージに一緒に入っています。

const webpack = require('webpack');

module.exports = {
  /* 色々 */
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'
    })
  ],
}

環境変数を渡すか渡さないかで切り替える

これは例えば、

yarn webpack

でビルドすればコード内のprocess.env.NODE_ENV === 'developmentになります。そして今度は、

NODE_ENV=production yarn webpack

のように環境変数を渡してビルドすればprocess.env.NODE_ENV === 'productionになるという感じです。このようにprocess.env.NODE_ENVの値を環境変数を渡すか渡さないかで切り替えることに成功したので、あとはコードの中で条件分岐などで処理を分けるだけですね。

他にどんなものを定義したらいいか

例えばこのサイトで使っている Contentful という CMS は 本番用と開発用でトークンが違うのでこういう別れてるものがメインで使えると思います。他には Stripe などのトークンも別れていますね。

ProvidePlugin を使って import を書く回数を減らす

例えば React を JSX で書いているプロジェクトであれば JSX を使っているファイルでは必ずimport React from 'react';のような書かなければいけません。( JSX 部分は変換されてReact.createElementを使うようになる為)

設定する

ProvidePluginwebpackモジュールに付属しているので新たにインストールする必要はありません。これをpluginsセクションの中に置きます。

{
  // ...略
  plugins: [
    new webpack.ProvidePlugin({
      React: 'react'
    })
  ]
}

ProvidePluginはオブジェクトを引数に取ります。その中にReact: 'react'と置いた場合、「reactモジュールをReactという名前でどこでも使えるようにするよ」という意味になります。JavaScript の場合設定はこれだけでどこでもReactが使えるようになっているはずです。

ちなみに、どこでもはwebpackのビルド対象ファイルだけで、window.Reactという形にはならないので安全です。
対象のファイルをラップする親スコープがあり、そこにreact-WEBPACK-IMPORTED-MODULE-1-defaultのような名前の関数が生えています。この関数は実行後にReactモジュールを返し、これをReactという名前で置く親スコープ内に置くことで使えるようになっているのだと思います。

ESLint を使っている場合はreact/react-in-jsx-scopeが引っかかるようになるかもしれません。これは"react/react-in-jsx-scope": 0と ESLint のファイルに追記して無効化できます。

TypeScript 対応

TypeScript でどこでもReactを使おうとすると、そのままではany型になってしまいます。コード上ではいきなり現れた謎変数な為です。これにはアンビエント宣言(Ambient Declarations)を定義したファイルを作る必要があります。その中身は「どこでも出てくるReactとは何ぞや」です。

アンビエント宣言はd.tsという拡張子で作ります。「どこでも出てくる」は、つまりglobalということなのでd.tsの置き場所はtsconfigincludeパターンに含まれるか、filesの中に置かれていればどこでも大丈夫です。

Reactだけならこれだけです。

import React from 'react';

// global 空間を拡張
declare global {
    const React: typeof React;
}

これでどこでも型付きReactが使えるようになったはずです。

ちなみにこっちは良くわからないですが駄目です。

import * as React from 'react';

declare var React: typeof React;

使った時にこのように警告されます。

'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.

「〜 import instead」この内容だと振り出しに戻るので却下です。なんだか難しいですが、モジュールのグローバル定義はdeclare globalで囲むことにします。

TypeScript の paths 設定が効くようにする

ルートに以下のようなtsconfig.jsonを置くだけではまだ WebPack で使えません。

{
  "baseUrl": ".",
  "paths": {
    "foo": ["src/foo/index.ts"]
  }
}

WebPack 設定にも手を加える

dividab/tsconfig-paths-webpack-pluginを使います。

これをresolve.pluginsに設定してあげます。pluginsではないので注意です。

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = {
  resolve: {
    plugins: [new TsconfigPathsPlugin(
      {
        // tsconfig.json はデフォルト
        configFile: 'tsconfig.json'
      }
    )]
  }
}

型情報有りで設定(webpack.config.js)ファイルを書く

VSCode での作業を想定してます。

JSDoc コメントの@type@typedefでは TypeScript の型ファイルを参照できます。これらのアノテーションを用いて値に型を付けると、例えばmodeの値を入力する時にdevelopmentproductionnoneがサジェストされるようになります。

webpack 型をインストール

webpackwebpack-cliはインストール済なら、もう1つ@types/webpackもインストールします。

yarn add -D @types/webpack

これで型定義部分でimport('webpack')のように書くと@types/webpackが提供する型を参照できるようになりました。

型を付ける

単に設定オブジェクト全体に型を付けたい場合はConfiguratiionを使います。ここで言う設定オブジェクトはoutputentrymoduleなど全てを持ったオブジェクトの事です。

オブジェクト

これは以下のように設定します。

/**
 * @type {import('webpack').Configuration}
 */
module.exports = {
  /* 設定の中身 */
};

これだけでmodeなどにサジェストされるようになったはずです。
Webpack の設定には配列で複数の設定オブジェクトを置いたり、関数で設定オブジェクトを置いたりといくつか別の形態を持ちます。

配列

配列の場合はconfigurationの後ろに[]を置くだけです。

/**
 * @type {import('webpack').Configuration[]}
 */
module.exports = {
  /* 設定の中身 */
};

関数

関数の場合は@typeではなく@returnsを使います。これにより戻り値の型を付けれます。

/**
 * @returns {import('webpack').Configuration}
 */
module.exports = () => ({
  mode: 'development'
});

ちなみにこの関数は、良くenvargvと名付けられる引数2つを受け取れますが、これらに型を付けたり場合は以下のようにします。

/**
 * @param {Object} env
 * @param {boolean} env.production
 * @param {Object} argv
 * @param {import('webpack').Configuration['mode']} argv.mode
 * @returns {import('webpack').Configuration}
 */
module.exports = (env, argv) => ({
  mode: env.production ? 'production' : 'development'
  // mode: argv.mode
  /* ... */
});

env{production: boolean}なオブジェクトで、argv{mode: string}のようなオブジェクトと型を付けしました。これでプロパティにアクセスする時にproductionmodeなどがサジェストされます。

プロミス

戻り値には設定オブジェクトを解決するプロミスでも良いので、その場合はこんな感じに。

/**
 * @returns {Promise.<import('webpack').Configuration>}
 */
module.exports = () =>
  new Promise(resolve => {
    resolve(/* ... */);
  });

共通設定を切り出す

共通設定を切り出すということは設定オブジェクトConfigurationから幾つかのプロパティを取り出して、必要なプロパティだけの新たなオブジェクト型を定義する必要があります。
当たらなオブジェクト型を定義するには@typedefアノテーションを使います。

例えばこのアノテーションは以下のようにすると{value: string}な型のオブジェクトをFooという名前で定義できます。Fooを使うときには、これまでと同じく@typeで使います。

/**
 * @typedef {Object} Foo
 * @property {string} Foo.value
 */

/**
 * @type {Foo}
 */
const foo = {value: 'something'};

設定オブジェクトが配列な時、modeというのは実行時に渡した環境変数などで決める内容なので切り分けると効率が良さそうです。それには{mode: ...}という内容の共通オブジェクトCommonWeConfigurationを定義して、適当な変数にその型を付けてあげます。

/**
 * @typedef {Object} CommonWebpackConfiguration
 * @property {import('webpack').Configuration['mode']} CommonWebpackConfiguration.mode
 */

/**
 * @type {CommonWebpackConfiguration}
 */
const common = {
  mode:
    process.env.NODE_ENV === "production"
      ? "production"
      : "development"
};

Configurationinterfaceですが、ここから更に各プロパティへアクセスするには['property']にインデックスにアクセスするような形で書きます。

HTMLWebpackPlugin

HTMLWebpackPlugin を使うとコンパイル時に、生成された JavaScript ファイルなどが読み込まれている状態の.htmlファイルが作れます。

インストール

以下でインストールします。

yarn add -D html-webpack-plugin

使い方

一番簡単な使い方は、ただ作成したインスタンスをplugins配列の 1 つとして渡すだけです。例えば以下のような設定があるとします。

const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')

module.exports = () => {
  return {
    mode: 'development',
    target: 'web',
    entry: {
      main: path.join(__dirname, 'index.js')
    },
    output: {
      path: path.join(__dirname, 'out'),
      filename: '[name].js'
    },
    plugins: [
      new HTMLWebpackPlugin(),
    }
  }
}

これを元にwebpackコマンドでビルドした時、output.path内のディレクトリにindex.htmlが作られます。またその.htmlファイルは[name].js、つまりmain.jsが読み込まれる記載のある状態で作られます。

インスタンスを作る際にはいくらか渡せるオプションがあります。

HTML ファイル名を変える

デフォルトではindex.htmlですが、これを変更したい場合はfilenameオプションを設定します。
以下のようなインスタンスを渡すと、先程のindex.htmldefault.htmlという名前で作られるようになります。

new HTMLWebpackPlugin({
  filename: 'default.html`
})

タイトルを変える

titleを設定することで、<title>タグの中身を好きなテキストに変更できます。
以下のようにすると<title>nju33<</title>という形で出力されます。

new HTMLWebpackPlugin({
  filename: 'nju33.html',
  title: 'nju33'
})

HTML マークアップを変える

templateにテンプレートにしたいファイルへの絶対パスを指定すると、そのファイルを元に.htmlファイルを作れます。
例えば以下はtemplate.htmlをテンプレートに指定している例です。

new HTMLWebpackPlugin({
  template: path.join(__dirname, 'template.html'),
  filename: 'template.html',
  title: 'template'
})

template.htmlはこのようになっています。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
</body>

</html>

このテンプレートファイルでは<%= ... %>により値が展開できます。またインスタンスのオプションがそのままhtmlWebpackPlugin.optionsに入っているので、上記ではtitle<title>タグの中に展開されるようなテンプレートになってます。

これをビルドすると<title>template</title>という形の HTML ファイルが作成されます。

ちなみに JavaScript ファイルは自動的に<body>タグの中に展開される為問題ないです。

JavaScript でテンプレートを作る

templateの代わりに、templateContentを使うこともできます。こちらに指定した関数の戻り値が.htmlファイルで出力されます。

この関数は第一引数のオブジェクトから HTML テンプレートのようにhtmlWebpackPluginのインスタンスが受け取れる為、それを使い自由にマークアップできます。
例えば以下のようにすると HTML テンプレートと同じマークアップを得られます。

new HTMLWebpackPlugin({
  templateContent: ({ htmlWebpackPlugin }) => {
    return `
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>${htmlWebpackPlugin.options.title}</title>
</head>

<body>
</body>

</html>
    `
  },
  filename: 'template-content.html',
  title: 'template-content'
})

これをビルドすると<title>template-content</title>という形の HTML ファイルが作成されます。

nju33-com/example-html-webpack-plugin をクローンすると上記をてっとり速く確認することができます。

その際は以下のコマンドを実行してください。

yarn
yarn dev
open http://localhost:33948/