Closed katai5plate closed 5 months ago
一応可能であればモジュール化までしていただきたいですが、難しい場合は
Game_Interpreter.prototype.pluginCommandText2Frame
の内部処理を分離して、
Game_Interpreter.prototype.pluginCommandText2FrameParser(text: string)
と
Game_Interpreter.prototype.pluginCommandText2FrameInjecter(options: {})
のように処理を分けていただき、
Game_Interpreter.prototype.pluginCommandText2FrameParser
をコンソール上から単体で実行できるようにしていただければ、それだけでも一応最低限の目的は達成できます。
そうすれば上記デモコードも以下のように書くことで実現できると思うので
// Text2Frame を実行し、関数定義
require("Text2Frame.js");
// JS から動的に生成されたテキスト
const date = new Date().toLocaleString();
const text = `<comment>
出力日時: ${date}
</comment>`;
// 実行
const list = Game_Interpreter.prototype.pluginCommandText2FrameParser(text);
console.log(list);
@katai5plate コメントありがとうございます。 いただいた要望ですが、実際にコードを用いながら内容の検討を行ったほうが良いと思いましたので、検討用の branch を作成し、私なりに要望の内容を反映した draft を push しました。(RPG Maker MV / MZ での動作は未確認です) https://github.com/yktsr/Text2Frame-MV/tree/117-forlib
const TF = require("./Text2Frame.js");
// JS から動的に生成されたテキスト
const place = ["城", "宿屋", "洞窟の前"][Math.floor(Math.random() * 3)];
const date = new Date().toLocaleString();
const text = `長老に会って挨拶は済ませてきたかい?
<ShowChoices>
<When: はい>
そうか。それならよかった。
早速長老の依頼のとおり、${place}に向かってくれないかい。
<When: いいえ>
それはいけない。
長老は君のような若者を探しているんだ。
挨拶に行って話を聞いてくれないかい。
<End>
<comment>
出力日時: ${date}
</comment>`;
// list 形式で出力される
const list = TF.convert(text);
// 挿入場所によって indent を変更したり、条件によって内容を変えたりできる
const result = list.map(({ indent, ...rest }) => ({
...rest,
indent: indent + 2,
}));
console.log(result);
※↑大したことではありませんが、parse -> convert に変わっています。
こちらのテストファイルを実行しますと
$ node test.js
[
{ code: 101, parameters: [ '', 0, 0, 2, '' ], indent: 2 },
{ code: 401, parameters: [ '長老に会って挨拶は済ませてきたかい?' ], indent: 2 },
{ code: 102, parameters: [ [Array], 1, 0, 2, 0 ], indent: 2 },
{ code: 402, parameters: [ 0, 'はい' ], indent: 2 },
{ code: 101, parameters: [ '', 0, 0, 2, '' ], indent: 3 },
{ code: 401, parameters: [ 'そうか。それならよかった。' ], indent: 3 },
{
code: 401,
parameters: [ '早速長老の依頼のとおり、宿屋に向かってくれないかい。' ],
indent: 3
},
{ code: 0, parameters: [], indent: 3 },
{ code: 402, parameters: [ 1, 'いいえ' ], indent: 2 },
{ code: 101, parameters: [ '', 0, 0, 2, '' ], indent: 3 },
{ code: 401, parameters: [ 'それはいけない。' ], indent: 3 },
{ code: 401, parameters: [ '長老は君のような若者を探しているんだ。' ], indent: 3 },
{ code: 401, parameters: [ '挨拶に行って話を聞いてくれないかい。' ], indent: 3 },
{ code: 0, parameters: [], indent: 3 },
{ code: 404, parameters: [], indent: 2 },
{
code: 108,
parameters: [ '出力日時: 12/13/2023, 2:24:25 AM' ],
indent: 2
}
]
になるかと思います。
@yktsr
ありがとうございます! コードを拝見しました。一応最低限欲しい機能は、作って頂いた実装で問題なく得られたと思います。
ESModule 化ですが、確かに内部で require を使っている以上、どうやっても import 文は使えませんね・・・
一応代替策として、 ビルドツールを用いて、ソースコードを ESModule で扱えるようにトランスパイルする という方法を提案してみます。
npm install --save-dev rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve
Text2Frame.js
を以下のように改修
if (typeof PluginManager === 'undefined') {
// for test
/* eslint-disable no-global-assign */
- Game_Interpreter = {}
- Game_Interpreter.prototype = {}
- $gameMessage = {}
- $gameMessage.add = function () {}
+ globalThis.Game_Interpreter = {}
+ globalThis.Game_Interpreter.prototype = {}
+ globalThis.$gameMessage = {}
+ globalThis.$gameMessage.add = function () {}
/* eslint-enable no-global-assign */
}
(function () {
'use strict'
const fs = require('fs')
const path = require('path')
const PATH_SEP = path.sep
- const BASE_PATH = path.dirname(process.mainModule.filename)
+ let BASE_PATH = "./";
+ if (globalThis.process !== undefined) {
+ if (globalThis.process.mainModule) {
+ BASE_PATH = path.dirname(process.mainModule.filename)
+ }
+ }
package.json
に次を追加
"types": "esm.d.ts",
ルートに esm.d.ts
を作成(説明文の内容はお任せします)
declare module "Text2Frame-MV/esm.mjs" {
/**
* ここに関数の説明文を書く
* @param text コマンドテキスト
*/
export function convert(
text: string
): { code: number; parameters: any[]; indent: number }[];
}
ルートに rollup.config.js
を作成
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
export default {
input: "./Text2Frame.js",
output: {
file: "./esm.mjs",
format: "esm",
sourcemap: true,
},
plugins: [
{
name: "replace-require-main",
transform(code, _id) {
return {
// `require.main` を `undefined` に置換する
code: code.replace(/require\.main/g, "undefined"),
map: null,
};
},
},
resolve(),
commonjs(),
],
};
必要に応じて、 CI のビルドプロセスにも追記してください
npx rollup -c
これで、ルートに esm.mjs
が出力されるはずです。
こうすれば、以下のように書けるようになります。
import TF from "./esm.mjs"
const date = new Date().toLocaleString();
const text = `<comment>
出力日時: ${date}
</comment>`;
console.log(TF.convert(text));
自分が使う場合、おそらく node_modules にこのリポジトリを入れて使うと思うので、 実際にはこうやって読み込むことになるかと。
npm i yktsr/Text2Frame-MV
import TF from "Text2Frame-MV/esm.mjs";
これは補足です。 どうしても、ES2020 の機能である globalThis を使いたくないという場合は、ポリフィルを次のように書くことができます。
var globalThis =
(typeof globalThis !== "undefined" ? globalThis : null) ||
(typeof global === "object" && global && global.Math === Math && global.Array === Array ? global : null) ||
(typeof self !== "undefined" ? self : null) ||
(typeof window !== "undefined" ? window : null) ||
Function("return this")();
コメント&提案ありがとうございます。 いただいたコメントを元に、全体的にコードを調整し、requireの影響を小さくした上でES module へトランスパイルして、117-forlibにpushしました。 こちらで所望の処理になるかと思います。
# npm install "yktsr/Text2Frame-MV#117-forlib"
import TF from "Text2Frame-MV/Text2Frame.mjs"
const date = new Date().toLocaleString();
const text = `<comment>
出力日時: ${date}
</comment>`;
console.log(TF.convert(text));
node Text2Frame.js
として直接実行されたときだけ実行される判定コードにしたいのですが、もしよい知見をご存知でしたらコメントいただけると大変助かります。
// developer mode
//
// $ node Text2Frame.js
if (typeof require !== 'undefined' && typeof require.main !== 'undefined' && require.main === module) {
@yktsr
require.main を使わずにターミナル実行を検出する方法として globalThis.process.argv
を使うのはどうでしょうか。
これで、ターミナル実行時の Node.js 本体のパス、実行スクリプトのパス、指定された引数を取ることができるので、
どのようにターミナル実行されたかどうかまで取ることができます。
それとこれは別解なのですが、developer mode は別ファイルに分けるのはどうでしょうか。
そもそも Node.js で使用する場合は、単体の JS をダウンロードしてきて実行するよりも、
npm install などで持ってきて使用することのほうが多いはずですので、npx などで実行できるようにしたり、
そこまでしなくても README に node node_modules/Text2Frame/node.js
を実行するように書いておくだけでもいいような気がします。
もちろん、Rollup でターミナル実行専用のビルドを書くのもいいでしょうし。
現代のWEB開発では、「ファイルを分けて開発し、ビルドで繋げる」という思想が一般的なので、 即戦力のために最初から1つのファイルで開発するよりも、 機能をバラで管理して、目的に合わせたビルドを行うほうがいいんじゃないかな、と思っていたりします。
それと、esm.d.ts が正しくリンクされていませんでした。
この画像のように types
または typings
を設定しないと、d.ts ファイルが関連付けられませんので注意です。
正しくリンクされているかどうかは、VSCode が認識しているかどうかで確認可能です。 VSCode を再起動して、問題なく認識するか確認できれば OK です。 (別タブで d.ts ファイルを開いていると、package.json の設定有無にかかわらず VSCode が勝手に認識してしまうので注意)
認識していない場合
認識している場合
ありがとうございます。
esm.d.ts
普通にコミット忘れていました。あとでやっておきます。(先行してコードだけ出したので)
それとこれは別解なのですが、developer mode は別ファイルに分けるのはどうでしょうか。 機能をバラで管理して、目的に合わせたビルドを行うほうがいいんじゃないかな、と思っていたりします。
これもそのとおりですね。 RPG Makerの制約で1ファイルにする必要がありましたが、本来はバラしてビルドして組み上げたほうが良かったかもしれません。最初はこれほどの構想ではなかったのもあります。
@yktsr
後から気づいたのですが、ビルドツールは Rollup ではなく、Vite のほうが良かったかもです。 Vite のライブラリモードならば、フォーマット指定でより洗練された形で CJS, ES モジュールに出力できるので。 一応、現状から Vite に移行する方法を書いておきます。
まず Text2Frame.mjs
を削除
rollup を削除し、vite をインストール
npm rm rollup
npm i -D vite
import { defineConfig } from "vite";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
export default defineConfig({
build: {
// ルートディレクトリに出力
outDir: ".",
lib: {
// ビルド元ファイル
entry: "./Text2Frame.js",
// ライブラリ名(umd 出力時のグローバル変数名)
name: "Text2Frame",
// 出力フォーマット
formats: [
"es", // ESM
"cjs", // CommonJS
"umd", // ブラウザ向け
],
// フォーマットごとのファイル命名方法
fileName: (format) => `Text2Frame.${format}.js`,
},
// rollup のプラグインを流用
rollupOptions: {
plugins: [resolve(), commonjs({ transformMixedEsModules: true })],
},
},
});
"script": {
"build": "vite build"
}
npm run build
これで、Text2Frame.cjs.js
と Text2Frame.es.js
と Text2Frame.umd.js
が出力されます。
namespace Text2FrameMV {
/**
* Text2Frameの文法で書かれた文字列をRPG Maker MV/MZのイベントコマンドリストに変換します。
* 戻り値にはMapの定義は含まれず、イベントコマンドリストのみが返るため、Mapへの組み込みは各自で行なってください。
* Converts strings written in Text2Frame syntax into RPG Maker MV/MZ event command lists.
* The return value only includes the event command list and does not contain the Map definition. Therefore, integration into the Map should be done individually.
* @param text Text2Frameの文法に従って書かれた文字列
*/
export function convert(
text: string
): { code: number; parameters: any[]; indent: number }[];
}
declare module "Text2Frame-MV" {
export = Text2FrameMV;
}
declare module "Text2Frame-MV/Text2Frame.cjs.js" {
export = Text2FrameMV;
}
declare module "Text2Frame-MV/Text2Frame.es.js" {
export = Text2FrameMV;
}
declare module "Text2Frame-MV/Text2Frame.umd.js" {
export = Text2FrameMV;
}
一応これは報告ですが、分離していただいた JSON 変換部を使って、 イベントコマンドを JS で書けるようにするライブラリ・プラグインを作りました。
https://github.com/katai5plate/Text2Frame-JS https://twitter.com/katai5plate/status/1738543507995054310
現状まだマージされていないので、github:yktsr/Text2Frame-MV#117-forlib
を直接インポートして使っています。
ありがとうございます。お待たせしていてすみません。 年末年始で検証作業するのでしばし、お時間ください!
@katai5plate おまたせしました。下記の変更を加えたのものを 117-forlib に push しました。
import TF from "./Text2Frame.es.js"
const date = new Date().toLocaleString();
const text = <comment> 出力日時: ${date} </comment>
;
console.log(TF.compile(text));
SyntaxError: Unexpected token 'export'
* 変換関数の名前をconvertからcompileへ変更
* こちら、大変申し訳無いのですが、名前を変えました。現在、Text2Frameの逆変換(既存のRPG Maker プロジェクトからText2Frameの構文に沿ったテキストを出力する)がunder review中で、こちらを decompile としたいためです。
* コマンドラインでも標準入出力を使って compile を呼び出せるように変更
* 不具合修正。グローバル変数、グローバル関数との依存を切りました。
RPG Maker MV 本体での動作確認は取れているため、MZでの確認をしてリリースになります。
@yktsr 拝見しましたが、これは commander が悪さしていますね・・・ やはり、ESMなどツクール向けのビルドでない場合、使わないコードは分離したほうが良さそうです。 もしくは commander を使わない代替策を使うとかですかね。
まず package.json に "type": "module",
を追記します。こうしないとNode.jsで import 記法が使えないためです。
次に、vite.config.js で build.minify
を false
にします。こうすることで、ビルド後のコードが読みやすくなります。
あとはビルドしなおして、デモコードを書いた test.js
を作成し、 node test
を実行します。
すると、以下のようなエラーが出ました。
class Command extends EventEmitter {
^
TypeError: Class extends value undefined is not a constructor or null
at file:///.../Text2Frame-MV/Text2Frame.es.js:980:23
at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:530:24)
at async loadESM (node:internal/process/esm_loader:91:5)
at async handleMainPromise (node:internal/modules/run_main:65:12)
この EventEmitter
を使用するライブラリを洗い出してみると、commander
が原因であることがわかります。
今度は、Text2Frame.js
の commander 使用部を全部コメントアウトし、ビルドしなおしてみます。
すると、問題なく実行することができるようになりました。
一応、process.argv を使って似たようなこともできるので参考にどうぞ
if (
typeof globalThis?.process !== "undefined" &&
typeof globalThis?.window?.nw === "undefined"
) {
// 引数
const args = process.argv.slice(2);
// 初期値
const options = {
mode: null,
text_path: null,
output_path: null,
event_id: null,
page_id: null,
common_event_id: null,
overwrite: "false",
verbose: false,
version: "2.2.1",
};
// 引数とオプション名
const modes = new Set(["map", "common", "compile", "test"]);
const optionMap = {
"-m": "mode",
"--mode": "mode",
"-t": "text_path",
"--text_path": "text_path",
"-o": "output_path",
"--output_path": "output_path",
"-e": "event_id",
"--event_id": "event_id",
"-p": "page_id",
"--page_id": "page_id",
"-c": "common_event_id",
"--common_event_id": "common_event_id",
"-w": "overwrite",
"--overwrite": "overwrite",
"-v": "verbose",
"--verbose": "verbose",
};
// 解析
args.forEach((arg, index, array) => {
const option = optionMap[arg];
if (option) {
if (option === "verbose") {
options[option] = true;
} else if (option === "overwrite") {
options[option] = array[index + 1] === "true";
} else if (
option === "mode" &&
!modes.has(array[index + 1].toLowerCase())
) {
console.error("mode の値が不正です: map, common, compile, test");
process.exit(1);
} else {
options[option] = array[index + 1];
}
}
});
console.log(options);
}
おまたせしました。一連の対応を本線に取り込みました。 結果的には、Text2Frame.js をビルドする際に、コマンドラインの部分を削除するようにしました。(各ビルド結果に不要なので) 本線に取り込んだ Text2Frame.es.js でお試ししてみてください!
要望です。 外部ツール開発や、派生プラグイン開発などをしやすくするために、
JSON 変換部と埋め込み処理部を切り離し、JSON 変換部をモジュールとして使えるようにしてほしいです。
以下は使用イメージのデモコードです。
これが実現すれば、次のようなことが可能になると思っています。
ぜひ、ご検討お願いします!