Home About Contact
InDesign , ExtendScript , TypeScript

TypeScript で書いたコードを ExtendScript から使う

以前に 「Markdown to InDesign 開発入門」 という キンドル本を書いたのですが、書き直そうと思い始めた。 理由のひとつは、 内容が古くなってしまったこと、そして もうひとつは、パーサーコンビネーターを使えばもっと簡単にマークアップテキストをInDesign ドキュメントに 変換できるのではないか?と思い始めたから。

この本では、markdown テキストをパースするために既存の markdown パーサーライブラリを使っている。 その markdown パーサーを ExtendScript として作動させることができないので、 普通の Node.js でつくった markdown テキスト to JSON変換サーバーを用意してそっちで変換する、 という方法を使っている。 既存の markdown パーサーライブラリであるためパーサーを自分で書く必要もないのは 圧倒的なメリットだが、かなりややこしい話になってしまった。 使う側にしたらテキストをパースして InDesign ドキュメントに変換したいだけなのに (ローカルで変換サーバー起動するとか面倒なことを・・・)。

今使えるかどうか確認しているパーサーコンビネータは TypeScript で記述されているので、 ここではまずそのための事前調査としてごく簡単な TypeScript を ExtendScript で実行できるか試してみます。

環境は Node.js を使います。

バージョンなど:

$ node --version
v18.17.1
$ npm --version
9.6.7

hello プロジェクトディレクトリを作成:

$ mkdir hello
$ cd hello

今回はライブラリとして文字列を渡すと文字単位の分割した上でその文字を Moji タイプにいれる というライブラリを考えます。

modules/moji.ts

$ mkdir modules
$ touch modules/moji.ts

moji.ts のコードを書く。

// moji.ts

type MojiKind = "JUST" | "BEGIN" | "END"

type Moji = {
    c?: string 
    kind: MojiKind
}

export const toMojiList = (text: string): Moji[] => {
    const begin: Moji = { kind: "BEGIN" }
    const end: Moji   = { kind: "END" }

    return [begin].concat( Array.from(text).map((c)=> {
        return { c: c, kind: "JUST" }
    }) ).concat( [end] )
}

toMojiList 関数はアプリケーション側のコードから使うことを想定しているので、 export const toMojiList と記述しています。 export を追加することでこの関数を公開できます。

なお、実際は Array.from() は ExtendScript では使えないので、あとで書きかえます。

まず TypeScript を JS に変換するために typescript を入れます。

$ npm install typescript@5.6.3 --save-dev

バージョンを 5.6.3 指定で。

それでは tsc コマンドで ts を js へ変換

$ npx tsc modules/moji.ts --checkJs --lib es2015

--lib es2015 オプションをつけないと以下のエラーが出ます。

error TS2550: Property 'from' does not exist on type 'ArrayConstructor'.

tsc 実行時に --checkJs オプションをつけることでタイプチェックをしてくれます。 たとえば、ここでは Moji タイプの属性 kind は MojiKind タイプで指定した 三種類の文字列しか受け付けないのですが、 もし、 "JUST" にかえて "HOGE" を指定したコードをかいて tsc すれば、 次のようにエラーを出して問題個所を教えてくれます。

modules/moji.ts:19:27 - error TS2322: Type '"HOGE"' is not assignable to type 'MojiKind'.

19     const end: Moji   = { kind: "HOGE" }

さて、次はこの moji ライブラリの toMojiList 関数を使う側のアプリケーションコードを main.js に書きます。

// main.js

const toMojiList = require('./modules/moji.js').toMojiList
console.log(toMojiList)

const helloMojiList = toMojiList("Hello")
console.log(helloMojiList)

現在の hello プロジェクトのファイル構成は次の通り:

.
├── main.js
├── modules
│   ├── moji.js
│   └── moji.ts
├── node_modules...
├── package-lock.json
└── package.json

main.js を実行して意図通り機能するか確かめます。

$ node main.js
[Function: toMojiList]
[
  { kind: 'BEGIN' },
  { c: 'H', kind: 'JUST' },
  { c: 'e', kind: 'JUST' },
  { c: 'l', kind: 'JUST' },
  { c: 'l', kind: 'JUST' },
  { c: 'o', kind: 'JUST' },
  { kind: 'END' }
]

意図通り作動しました。

moji.ts から moji.js に変換して、それを main.js から使う、そして main.js を node で実行という流れです。 ここまでの、ビルドして実行するところまでの手順を Makefile に書いておきます。

run: modules/moji.js
	node main.js

modules/moji.js: modules/moji.ts
	npx tsc modules/moji.ts --checkJs --lib es2015

clean:
	rm -f modules/moji.js

main.js をExtendScript として作動するようにする

ここまでで TypeScript で書いた moji.ts ライブラリを main.js で利用して node で実行まではできました。

ここから、この main.js を ExtendScript で作動するように変換します。

なお詳細は 「Node.js による InDesign ExtendScript モダン開発入門2024」 というキンドル本に書いたのでそちらをご覧ください。

まずは main.js を ExtendScript として作動するように変換します。 ExtendScript は require や console.log など使えないので、その辺を対処します。

console.log がない問題は main.js の先頭に次のコードを追加して回避:

// main.js

const console = {}
console.log = (message) => {
    $.writeln(message)
}

次は、require がないとかの問題に対処します。 変換に必要なライブラリを入れます。

$ npm install browserify@17.0.0 --save-dev
$ npm install @babel/core@7.23.2 --save-dev
$ npm install @babel/cli@7.23.0 --save-dev
$ npm install @babel/preset-env@7.23.2 --save-dev

babel.config.json を用意:

$ touch babel.config.json

内容は:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true,
        "modules": false
      }
    ]
  ],
  "plugins": []
}

この段階でのプロジェクトのファイル構成確認:

.
├── Makefile
├── babel.config.json
├── main.js
├── modules
│   ├── moji.js
│   └── moji.ts
├── node_modules...
├── package-lock.json
└── package.json

それでは main.js を変換して main.jsx にします。

$ npx browserify main.js --outfile tmp.js
$ npx babel tmp.js --out-file main.jsx

browserify してから babel する感じです。

実はまだこれで ExtendScript として作動しないのですが、 その問題はあとで修正するとして、ここでいったん VSCode + ExtendScript Debugger で main.jsx を実行してみます。

VSCode と ExtendScript Debugger

VSCode と ExtendScript Debugger を使うので hello プロジェクトに .vscode/launch.json を用意します。

$ mkdir .vscode
$ touch .vscode/launch.json
$ cat launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "extendscript-debug",
            "request": "launch",
            "name": "main.jsx",
            "script": "${workspaceFolder}/main.jsx",
            "hostAppSpecifier": "indesign-19",
            "engineName": "main"
        }
    ]
}

それでは VSCode 起動。( hello プロジェクトのルートで code . する)

$ code .

あとはデバッグ実行するだけです。(まあ作動しないのですが。)

Object.defineProperty is not a function

Object.defineProperty is not a function というエラーに遭遇します。

解決するには、extendscript-es5-shim を使います。

$ npm install extendscript-es5-shim@0.3.1

ちなみに現時点での package.json はこんな感じです。

{
  "devDependencies": {
    "@babel/cli": "^7.23.0",
    "@babel/core": "^7.23.2",
    "@babel/preset-env": "^7.23.2",
    "browserify": "^17.0.0",
    "typescript": "^5.6.3"
  },
  "dependencies": {
    "extendscript-es5-shim": "^0.3.1"
  }
}

main.js の先頭に次のコードを追加:

require('extendscript-es5-shim')

main.js 全体はこのようになりました。

require('extendscript-es5-shim')

const console = {}
console.log = (message) => {
    $.writeln(message)
}

const toMojiList = require('./modules/moji.js').toMojiList
console.log(toMojiList)

const helloMojiList = toMojiList("Hello")
console.log(helloMojiList)

それでは変換して main.jsx を作り出す必要がありますが、タイプするのが面倒なので、 Makefile に main.js から main.jsx を作り出すビルド手順をかきます。

main.jsx: tmp.js modules/moji.js
	npx babel tmp.js --out-file main.jsx 

.INTERMEDIATE: tmp.js
tmp.js: main.js
	npx browserify main.js --outfile tmp.js

modules/moji.js: modules/moji.ts
	npx tsc modules/moji.ts --checkJs --lib es2015

clean:
	rm -f modules/moji.js

再び VSCode で ExtendScript Debugger デバッグ実行します。

Array.from is not a function

先ほどの問題は回避できたのですが、 今度は Array.from is not a function エラーになりました。

Array.from という安全に(サロゲートペアがどうのといった問題なく)文字に分割できる関数が ExtendScript で使えない。 仕方ないので、modules/moji.ts の toMojiList 関数を修正します。

export const toMojiList = (text: string): Moji[] => {
    const begin: Moji = { kind: "BEGIN" }
    const end: Moji   = { kind: "END" }

    /*
    return [begin].concat( Array.from(text).map((c)=> {
        return { c: c, kind: "JUST" }
    }) ).concat( [end] )
    */

    const list = []
    const len = text.length
    for(let i=0; i<len; i++){
        const moji: Moji = {
            c: text[i],
            kind: "JUST"
        }
        list.push(moji) 
    }
    return [begin].concat(list).concat([end])
}

このコードで全ての文字を正しく意図通りに1文字ずつに分割できるかどうかはまた別問題ですが。今はそこまでは考えないこととします。

最後に main.js に show 関数を追加します。(コンソール出力を整えるだけのためです。)

const show = (mojiList)=> {
    for(let i=0; i<mojiList.length; i++){
        const m = mojiList[i]
        console.log(`- ${m.kind} ${m.c}`)
    }
}

完成した main.js コード全体:

require('extendscript-es5-shim')

const console = {}
console.log = (message) => {
    $.writeln(message)
}

const show = (mojiList)=> {
    for(let i=0; i<mojiList.length; i++){
        const m = mojiList[i]
        console.log(`- ${m.kind} ${m.c}`)
    }
}

const toMojiList = require('./modules/moji.js').toMojiList
//console.log(toMojiList)

const helloMojiList = toMojiList("Hello")
console.log(show(helloMojiList))

それでは make して main.jsx を生成します。

$ make

あとは VSCode で実行します。

hello

作動しました。(コンソールに結果が出るだけですけど。)

まとめ

TypeScript で書いたライブラリを JavaScript に変換して さらに Browserify + Babel で変換することで ExtendScript として作動させることができることがわかりました。

ただし Array.from が使えなかったように地雷はある。 自分で書いたTypeScriptのコードならばその辺を注意すればよいが、 一般公開されているコードまでこの方法で使えるようになるわけではない。

そのあたりまで対応したいのであれば、いよいよ UXP を使うべきなのであろう。