Underscore.js を以前は Node.js + Babel + Webpack を使って 一つの jsx ファイルにまとめて使っていた。 しかし、時が経ち Node.js 自体もバージョンアップされモジュールもバージョンアップ・・・ なんやかんやで今や最新の環境ではうまく作動しないことがわかった。
InDesign も UXP が使えるようになったので、 ExtendScript で ES6 記述ができるとか今さら・・・とも思うのですが、ビジネスにおいては まだ当分の間 ExtendScript が使われていく気がするので、 最新の Node.js 環境で ExtendScript 用に Underscore.js などのモジュールの利用、 そして ES6 スタイルでの const, let, テンプレートリテラル, アロー関数などの構文が記述できる状態にしたい。
ごく簡単なコードでしか検証していませんが、 やり方がわかったので、備忘録としてシェアします。
以下のような Underscore.js を使ったコードを ExtendScript として InDesign のJS(というかExtendScript)エンジンで動かすことを考えます。
var _ = require("underscore");
console.log("--- each 1 ---");
const list = [1,2,3];
_.each( list, function(it){
    console.log("- " + it);
});
console.log("--- each 2 ---");
const listList = [[1,2,3], [3,2,1]];
_.each( listList, function(it){
    console.log("- " + it);
});
console.log("--- flatten ---");
_.each( _.flatten(listList), function(it){
    console.log("- " + it);
});
console.log("--- flatten and uniq ---");
_.each( _.uniq(_.flatten(listList)), function(it){
    console.log("- " + it);
});
今回テストに使う環境。
$ node --version
v18.12.1
$ npm --version
8.19.2
myunderscore というプロジェクトディレクトリを作成して初期化します。
$ mkdir myunderscore
$ cd myunderscore
$ npm init -y
そして、必要なモジュールを入れます。
$ npm install underscore
package.json で インストールされた Underscore.js のバージョンを確認。
$ cat package.json
{
  "name": "myunderscore",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "underscore": "^1.13.6"
  }
}
1.13.6 が入りました。
必要なら npm install underscore@1.13.6 のようにしてバージョンを指定して入れましょう。 このエントリーでは今時点での最新のモジュールを入れることにして、以降もバージョンは明示せずにインストールしていきます。
先程のコードを main.js に保存して実行します。
$ node.main.js
--- each 1 ---
- 1
- 2
- 3
--- each 2 ---
- 1,2,3
- 3,2,1
--- flatten ---
- 1
- 2
- 3
- 3
- 2
- 1
--- flatten and uniq ---
- 1
- 2
- 3
Node.js では、 意図通り作動しました。
$ npm install rollup --save-dev
rollup はバージョン 4.1.4 が入りました。
あとは ExtendScript として作動させたときにも console.log できるよう以下を追加。
const console = {};
console.log = function(message){ $.writeln(message); };
さらに、rollup は CommonJS 形式に対応していなかった気がするので、 require で underscore を指定しているコードを ESM 形式に変更。
//var _ = require("underscore");
import _ from "./node_modules/underscore/underscore-esm.js";
試してはいないのだが @rollup/plugin-commonjs あたりを 使えば、require 記述のままで rollup できるのかもしれない。 しかし、後述するように (現時点での考えに過ぎないが)Rollup.js は使わないで extendscript-es5-shim + Browserify + Babel を使うことにしたので、Rollup.js 関連の深追いはしない。
変換実行。
$ npx rollup main.js --file main.jsx
main.jsx を見ると underscore-esm.js がバンドルされて巨大なファイル(2055行)になっています。
$ wc main.jsx
2055    9213   64839 main.jsx
これが ExtendScript として InDesign で作動するか確認してみます。
VSCode + ExtendScript Debugger を使って実行したいので、myunderscore プロジェクトディレクトリ直下に .vscode/launch.json をつくります。
$ mkdir .vscode
$ touch .vscode/launch.json
.vscode/launch.json の内容は次の通り。
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "extendscript-debug",
            "request": "launch",
            "name": "main.jsx",
            "script": "${workspaceFolder}/main.jsx",
            "hostAppSpecifier": "indesign-19",
            "engineName": "main"
        }
    ]
}
main.jsx を indesign-19 (InDesign 2024) で実行するように設定しています。
それでは code . として VSCode を起動して ExtendScript Debugger (v2.0.3) で実行してみます。
VSCode で ExtendScript を実行する方法の説明は割愛します。

普通に作動しました!
別のコードで試したときにうまく作動しなかった気がしたのですが・・・。 このコードでは問題ないようですね。
今度は extendscript-es5-shim + Browserify を試します。
$ npm install extendscript-es5-shim
$ npm install browserify --save-dev
browserify は --save-dev します。
package.json の依存部分を確認。
{
  "dependencies": {
    "extendscript-es5-shim": "^0.3.1",
    "underscore": "^1.13.6"
  },
  "devDependencies": {
    "browserify": "^17.0.0"
  }
}
Rollup.js は外しています。
main.js を extendscript-es5-shim と Browserify 用に書きかえます。
require("extendscript-es5-shim");
//var _ = require("underscore");
//import _ from "./node_modules/underscore/underscore-esm.js";
var _ = require("./node_modules/underscore/underscore-node.cjs");
Underscore.js については再び ESM 形式から CommonJS に戻すのですが、 underscore のみの指定ではなく、 ./node_modules/underscore/underscore-node.cjs CommonJS 用のそれを直接指定します。
それでは browserify コマンドを使って、main.js を main.jsx に変換します。
$ npx browserify main.js --outfile main.jsx
main.jsx を見ると依存モジュールがバンドルされて巨大なファイル(3618行)になっています。
$ wc main.jsx
3618   15348  115325 main.jsx
これで main.jsx ファイルに extendscript-es5-shim と Underscore.js がバンドルされた状態になりました。
それでは code . として VSCode を起動して ExtendScript Debugger (v2.0.3) で実行します。 問題なく作動します。先程と同じ結果になるだけなので実行結果の詳細は割愛します。
Babel は(他にもさまざまな機能がありますが、今ここで関心があるの機能として) ES6 でしか使えない構文を ES5 に変換してくれます。
そこで、ES6 固有の構文 const, let, アロー関数, テンプレートリテアラル を使って main.js を書きかえます。
require("extendscript-es5-shim");
const _ = require("./node_modules/underscore/underscore-node.cjs");
const console = {};
console.log = (message)=>{ $.writeln(message); };
console.log("--- each 1 ---");
const list = [1,2,3];
_.each( list, (it)=> console.log(`- ${it}`) );
console.log("--- each 2 ---");
const listList = [[1,2,3], [3,2,1]];
_.each( listList, (it)=> console.log(`- ${it}`) );
console.log("--- flatten ---");
_.each( _.flatten(listList), (it)=> console.log(`- ${it}`) );
console.log("--- flatten and uniq ---");
_.each( _.uniq(_.flatten(listList)), (it)=> console.log(`- ${it}`) );
ここからさらにリファクタリングしてもう少しだけクリーンなコードにします。
require("extendscript-es5-shim")
const _ = require("./node_modules/underscore/underscore-node.cjs")
const console = {}
console.log = (message)=>{ $.writeln(message) }
const stdout = (it)=> console.log(`- ${it}`)
console.log("--- each 1 ---")
const list = [1,2,3]
_.each( list, (it)=> stdout )
console.log("--- each 2 ---")
const listList = [[1,2,3], [3,2,1]]
_.each( listList, (it)=> stdout )
console.log("--- flatten ---")
_.each( _.flatten(listList), stdout )
console.log("--- flatten and uniq ---")
_.each( _.uniq(_.flatten(listList)), stdout )
これで main.js の準備は完了です。
次に変換のためのに Babel を使うのでそれをインストールします。
$ npm install --save-dev @babel/core @babel/cli @babel/preset-env
package.json の依存部分を確認。
{
  "dependencies": {
    "extendscript-es5-shim": "^0.3.1",
    "underscore": "^1.13.6"
  },
  "devDependencies": {
    "@babel/cli": "^7.23.0",
    "@babel/core": "^7.23.2",
    "@babel/preset-env": "^7.23.2",
    "browserify": "^17.0.0"
  }
}
Babel の 7.23.x バージョンが入りました。
Babel の設定を babel.config.json に書きます。
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true,
        "modules": false
      }
    ]
  ],
  "plugins": []
}
それでは変換します。 まず、browserify でバンドルしてそれを tmp.js に保存、その後 tmp.js を babel で変換(ES6構文を ES5に)する流れです。
$ npx browserify main.js --outfile tmp.js
$ npx babel tmp.js --out-file main.jsx
main.jsx を見ると依存モジュールがバンドルされて巨大なファイル(3419行)になりました。
$ wc main.jsx
3419   15566  132787 main.jsx
main.jsx にある もとは ES6 構文で記述されたコードが ES5 の構文に変換された部分を見てみます。
    require("extendscript-es5-shim");
    var _ = require("./node_modules/underscore/underscore-node.cjs");
    var console = {};
    console.log = function (message) {
      $.writeln(message);
    };
    var stdout = function stdout(it) {
      return console.log("- " + it);
    };
    console.log("--- each 1 ---");
    var list = [1, 2, 3];
    _.each(list, function (it) {
      return stdout;
    });
    console.log("--- each 2 ---");
    var listList = [[1, 2, 3], [3, 2, 1]];
    _.each(listList, function (it) {
      return stdout;
    });
    console.log("--- flatten ---");
    _.each(_.flatten(listList), stdout);
    console.log("--- flatten and uniq ---");
    _.each(_.uniq(_.flatten(listList)), stdout);
const が var に、 アロー関数が普通の function 記述に、テンプレートリテラルもなくなっています。
この main.jsx を VSCode で開いて ExtendScript Debugger で実行すると普通に実行できました。
最後にこの手順を Makefile に記述しておきます。
Makefile
main.jsx: tmp.js babel.config.json
	npx babel $< --out-file $@
.INTERMEDIATE: tmp.js
tmp.js : main.js
	npx browserify $< --outfile $@
clean:
	$(RM) main.jsx
そして、最終的な package.json は以下のようになりました。
{
  "name": "myunderscore",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "extendscript-es5-shim": "^0.3.1",
    "underscore": "^1.13.6"
  },
  "devDependencies": {
    "@babel/cli": "^7.23.0",
    "@babel/core": "^7.23.2",
    "@babel/preset-env": "^7.23.2",
    "browserify": "^17.0.0"
  }
}
extendscript-es5-shim + Browserify + Babel を使えば、ES6 の構文を使って ExtendScript が記述できる。 しかも、Underscore.js などのモジュールも使える。 さらに Babel により、ES6 構文までサポートされるので、多くの Node.js 用のモジュールが使える可能性がある。
もっとも、構文以外の障害として 使いたいモジュールが ExtendScript にはない・・・ というか正確には InDesign の ExtendScript エンジンにはない Node.js 固有の機能に依存していれば、 そのモジュールは ExtendScript で使えない。 という意味では、そんなに使えるモジュールは多くないかもしれません。 しかし、自分にとっては Underscore.js が使えれば十分なのでこれでよい。
なお、あとで気づいたのですが、extendscript-es5-shim を入れると JSON オブジェクトが存在するようになって、 JSON.parse() や JSON.stringify() が使えるようになる。これは楽。