まえがき

PCのリニューアルにつき、旧PCのローカルに残ってたメモやらブログ用下書きの整理をしていたら、かつて自分がハマったトラブルの対応時のメモが発掘されました。読み返してみたら懐かしい気持ちになれたり、当時考えていたことが思い出せたりといいことが多かったので、晒してみようと思います。

ちなみにメモのタイトルには『webpackで動的module解決しようとして死にかけた』と書いてありました。

※ 2017年上旬に残したメモなので、内容に古い箇所があったり引用文とリンク先の文章にズレがあったりする可能性があります。

メモ内容

こまったことになったぞ

let modulePath;
switch (flag) {
  // 省略
  // modulePathを動的に決定する
}
let Constructor = require(modulePath);
let instance = new Constructor();

ということをしようとするとエラーになる。

Error: Cannot find module "."

modulePathが文字列としてちゃんと認識できていないのかと思ったけどそうでもなかった。

// 検証1
let Constructor = require(modulePath + '');  // 同様のエラー

// 検証2
console.log(typeof modulePath);  // string

もしやmodulePathは正規表現じゃないとだめ? とか血迷ってみた。

let modulePath = /model\/userlist/;
let Constructor = require(modulePath);  // やはりエラー

ここでようやくwebpackのモジュール解決に起因すると気づく。

Keep in mind that import() path cannot be fully dynamic (e.g., import(Math.random())). Rather either completely static (e.g., import(‘./locale/de.json’)) or partially static (e.g., import(‘./locale/’ + language + ‘.json’)). Code Splitting - Using import()

A context is created if your request contains expressions, so the exact module is not known on compile time. Dependency Management

require with expression 訳してみる(Google翻訳)

A context is created if your request contains expressions, so the exact module is not known on compile time.

リクエストに式が含まれている場合、コンテキストが作成されるため、コンパイル時に正確なモジュールが認識されません。

Example:

require("./template/" + name + ".ejs");

webpack parses the require() call and extracts some information:

webpackはrequire()呼び出しを解析し、いくつかの情報を抽出します。

Directory: ./template
Regular expression: /^.*\.ejs$/

context module ってなんだ

A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression. The context module contains a map which translates requests to module ids.

コンテキストモジュールが生成されます。このディレクトリには、正規表現に一致する要求が必要な、そのディレクトリ内のすべてのモジュールへの参照が含まれています。コンテキストモジュールには、要求をモジュールIDに変換するマップが含まれています。

Example:

{
    "./table.ejs": 42,
    "./table-row.ejs": 43,
    "./directory/folder.ejs": 44
}

The context module also contains some runtime logic to access the map.

This means dynamic requires are supported but will cause all possible modules to be included in the bundle.

コンテキストモジュールには、マップにアクセスするためのランタイムロジックも含まれています。 これは動的要件がサポートされているが、すべての可能なモジュールがバンドルに含まれることを意味します。

context module API

A context module exports a (require) function that takes one argument: the request. The exported function has 3 properties: resolve, keys, id.

コンテキストモジュールは、1つの引数(request)を受け取る(require)関数をエクスポートします。 エクスポートされた関数には、resolve、keys、idという3つのプロパティがあります。

突如解決する

webpack1のドキュメントにこんな文章を見つける。

If the module source contains a require that cannot be statically analyzed, the context is the current directory. In this case a Critical dependencies warning is emitted. You need to use the ContextReplacementPlugin in most cases. Examples: someFn(require) require.bind(null)

静的解決できないrequireがソースに含まれている場合、そのときのcontextはカレントディレクトリとなる。

let Constructor = require(modulePath);  // ← modulePathがカレントからの相対パスでないとダメ…?

実はモジュールパス解決をしたくてwebpack.config.jsに手を入れていた。

module.exports = {
  // 省略
  resolve: {
    extensions: ['.vue', '.js' ],
    // root: [ path.resolve('./js') ], // webpack 1
    modules: [
      path.resolve(__dirname + '/js'), // webpack 2
      path.resolve(__dirname + '/node_modules')
    ]
  }

path.resolveで指定したディレクトリをルートとした相対パスで書けるようにしていた(つもりだった)が、実際はカレントディレクトリからのパスが必要だった。

let Constructor = require('../' + modulePath);  // 動いた

更になんか見つける

Module

These options describe the default settings for the context created when a dynamic dependency is encountered.

これらのオプションは、動的依存関係が発生したときに作成されるコンテキストのデフォルト設定を記述します。

どうやら今回のように動的依存解決が発生している場合の「デフォルト」が内部的に指定されている模様。そしてそれを書き換えるプラグインもある模様(ただしDeprecated)。

module: {
  exprContextCritical: true,
  exprContextRecursive: true,
  exprContextRegExp: false,
  exprContextRequest: ".",
  unknownContextCritical: true,
  unknownContextRecursive: true,
  unknownContextRegExp: false,
  unknownContextRequest: ".",          // ←エラーの文言で見覚えあるぞ!!!
  wrappedContextCritical: false
  wrappedContextRecursive: true,
  wrappedContextRegExp: /.*/,
}

Note: You can use the ContextReplacementPlugin to modify these values for individual dependencies. This also removes the warning.

ContextReplacementPluginを使用して、個々の依存関係のこれらの値を変更できます。これにより、警告も削除されます。

ふむふむ🤔
とりあえず動いてよかった。