質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.07%

WSH/JScriptでもrequire()ライクなモジュール管理をしたい

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 180

torigara

score 12

前提・実現したいこと

WSH/JScriptでも、Node.jsにおけるrequire()のように細かく機能別にモジュールとして分割管理をしたいと考えております。
最終的には、本家のrequire()と同等に動作する関数を用意することで、作成したモジュールをNode.jsでも共用利用できることを目指しています。

元々は、WSFを採用することで、モジュール単位で複数に分割したファイルを<script>タグで呼び出すという形で上記の目的の一部は達成できていました。
しかし、Node.jsとの共用利用という意味では各モジュールをUMD化する必要性もあるし、何よりモジュールから別のモジュールを呼び出すということができません。
そのため、ADODB.Streamを利用してファイルを読み込んでeval()することにより、疑似的に動作する関数を定義してみたのですが、モジュールから別のモジュールを呼び出す際のパス指定において問題が生じたので質問させていただきます。

発生している問題

読み込み対象が絶対パス、もしくはWSFファイルを起点とした相対パスで指定された場合には、希望通り動作します。
一方で、モジュールから別のモジュールを呼び出すといった場合に、本家のrequire()の場合は「呼び出し元を起点とした相対パス」で呼び出し先を指定できますが、現状ではWSFファイルを起点とした相対パスでしか指定できないといった問題が生じており、モジュールの再利用性が損なわれています。

該当のソースコード

ファイル構成

current
  L module
  |   L echo
  |   |   L echo.js
  |   |
  |   L test.js
  |
  L require-test.js
  L require-test.wsf

require-test.wsf

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<package>
<job>
  <script language="JScript" src="require-test.js"></script>
  <script language="JScript"><![CDATA[
    var test = require('module/test.js');
    test();

    var echoFromCache = require('module/echo/echo.js');
    echoFromCache('echo by cached module');  // キャッシュの再利用性の確認
  ]]></script>
</job>
</package>

require-test.js

var require = function(modulePath) {
  var moduleName = modulePath.replace(/^(?:.*?[\/\\])?([^\/\\]+?)(?:\.js)?$/, '$1');  // キャッシュのためのモジュール名

  // 1.キャッシュから呼び出し
  if (require.cache.hasOwnProperty(moduleName)) {
    WScript.Echo('"' + moduleName + '" is cached');
    return require.cache[moduleName];
  }

  // 2.ファイルから読み込み
  var readFile = function(filePath) {
    var st = new ActiveXObject('ADODB.Stream');
    st.mode = 3;  // 読み取り/書き込み両方モード
    st.type = 2;  // テキストデータ
    st.charset = 'UTF-8';
    st.Open();
    try {
      st.LoadFromFile(filePath);
      var textData = st.ReadText();
    } catch(e) {
      throw new Error(e.message + '\n' + 'filePath = ' + filePath);
    }
    st.Close();
    return textData;
  };

  // TODO: Node require のパス探査機構の構築
  // 1.上位ディレクトリをたどって、node_modules/{MODULE} を探す。
  // 2.拡張子省略時に、{MODULE}.js -> {MODULE}.json -> {MODULE}/index.js -> {MODULE}/index.json も探す。
  var loadedString = readFile(modulePath);

  var evalCode = (
    '(function() {' +
    /**/'var _module = {}, _exports = _module.exports = {};' +
    /**/'(function(require, module, exports) {' +
    /******/loadedString +
    /**/'})(require, _module, _exports);' +
    /**/'return _module.exports;' +
    '})();'
  );
  var evalModule = eval(evalCode);  // module.exports

  WScript.Echo('"' + moduleName + '" load from file');
  return require.cache[moduleName] = evalModule;  // キャッシュ
};

require.cache = {};  // モジュールのキャッシュ先

module/test.js

var echo = require('module/echo/echo.js');  // wsfファイルのあるディレクトリ基準となってしまう
// var echo = require('echo/echo.js');  // test.jsからみた相対パス指定したい
var test = function() {
  echo('echo by test-function');
};
module.exports = test;

module/echo/echo.js

module.exports = function echo(text) {
  WScript.Echo(text);
};

試したこと

WScript.ScriptFullName で取得できるのはWSFファイルのパスであり、各スクリプトファイルのパスを取得する方法がわかりません
上の例でいうと、test.jsのパスがわかれば、echo.jsへの相対パスを絶対パスに変換したり、カレントを変更してしまうといったことで対応できます。
Node.jsでいうところの __dirname が分かれば早いのですが・・・。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+1

私も同じ気持ちを持っていたことがあるのですが結構苦労して、実行していました。が、最近その方向性を諦めました。

モジュール合算にはwebpack/rollup/parcel/browserifyなどのモジュールバンドラーのようなものを用いた方がいいと思います。nodeはモジュラーを自前でもっていますが、やや古いブラウザは持っていないのでそのためのwebpack等です、それのwsh版としてそれらが使えるか、あるいは自前で作るのがよいかもです。

私の作成しているライブラリ Parts.js ではver5系までwsh対応していました。denoに対応するためにver6以降はwsh対応を諦めました。babelの吐くソースがどうやってもwshで動かずだったです。

これはbabel&webpackで1ファイルにまとめてから利用するライブラリです。

https://github.com/standard-software/partsjs/tree/v5.8.2/test_exec/release_wsh

webpack出力したコードがes3(wsh相当)に対応しないために、polyfillしないといけなかったりして、やっかいですがなんとか動かしていました。rollupとか、percelがもっとよいかもしれません。試せてません。

また、それ以前のstsLib.js というライブラリも作っていました。こちらはbabel関係なくrequireの偽装化してモジュール化しています。このあたり見てみてください。
https://github.com/standard-software/stsLib.js/tree/master/Source/stsLib.js/test

nodeやbrowserifyともソースコード共用しています。
wsh用のrequireでは、パス読み出しはせずに命名でグローバルオブジェクトにexportの内容を登録しています。
で、結局ファイルを合算するためにライブラリ利用側のコードを修正することになるので、一応仕組みは実現したのですが、あまりファイルごとでのモジュール分割はできませんでした。なので、stslib_core.js ってところにやたらコードが集中しています。

質問で頂いているパスの問題は解決できず、requireとして相対パスでファイルを読み込もうとしても、jsのエンジン(cscript.exeとかかな)が今動いているjsソースがどのパスにあるのかを検知する方法がないので、文字列登録でグローバルオブジェクトに登録して、requireで呼び出す仕組みです。

現代的なJSコードにはES最新化するためのBabel(あるいはTypeScript)は欲しくなるので、結局ビルド環境は必要になりますし、denoや、babel/webpack ではすでにimport&exportの時代になりつつ、こいつらはJS構文でどうしようもない感じもあるので最初に記載したように、トランスパイラ、モジュールバンドラーで環境を構築しての実装をおすすめします。

あるいは node に移行したほうが絶対幸せ感があるとも思います。

似たような別件ですが、下記のような質問をして回答もらったこともあります。WSHでがんばろう的なエンジニアは日本に何人いるんだろうと感じます。応援します。

javascript - @babel/polyfill が非推奨なので core-js に移行しようとしたが WSH 環境でうまく動かない - スタック・オーバーフロー
https://ja.stackoverflow.com/questions/65439/

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/06 16:27

    回答ありがとうございます。
    以前よりWSH用のrequire()を模索されていた方からの回答とのことで、相対パスによる読み込みを諦める決心がつきました。
    リンク先は本業が少し立て込んでいるため、まだザっとしか拝見できていませんが、今後の参考にさせていただこうかと思います。

    一応、今後似たような思想を持った方がいたら参考になるように、今現在の方針を羅列しておきます。
    Node.jsとWSHで共用利用できるように、require()には呼び出し元のファイルからの相対パスを引数として渡す。
    WSH用のrequire()からは、引数を元にしたファイル呼び出しは行わない。
    モジュールは、その相関関係をすべて把握した上で、すべて<script>タグから呼び出す。
    モジュール自体はNMD化して、WSH用の分岐を用意。<script>タグから読み込む際の登録先をグローバルではなくrequire.cacheにしてグローバル汚染を防ぐ。
    発想としては、モジュールが必要になった時点でrequire()実行によるファイル呼び出しをするのではなく、事前にすべて<script>タグでロードしておき、require()を実行した際にはモジュールの内容をcacheから参照するだけとする。

    なにかアドバイス等あるなら、ご意見お待ちしております。

    キャンセル

0

追加ですが、

モジュール分割された jsファイルをビルドして結合して、wshで動かすことのできるものにする環境をGitHubにアップしました。

node を使って、babel/webpack でビルド(1ファイルにまとめる)しています。

Project01 は CommonJS 形式でファイルをリンクしてnodeとwebとwsfで動かしています。
Project04 は ESModule 形式でファイルをリンクしてdenoとnodeとwebとwshで動かしています。

https://github.com/standard-software/parts-Node_Deno_ProjectTemplate/tree/v1.4.0/Backup

これで、最新のJSの構文でWSHでも動かすことができると思います。

先に紹介したParts.js でビルドを試していたところ、
DenoとWSHに両対応できなかったので、無理だと思い込んでいたのですが、何か別の要因だったみたいです。Parts.jsはソースが大きいので対応しない構文をつかっていてビルドが失敗したのかも。

上記のプロジェクトで最小限のビルド環境を整えて試したところ、Deno/Node/Browser/WSH、すべて動きました。

node/npm に使い慣れている人なら、scripts内のコマンドを順次実行していってビルドできるようにしています。

あまり整っていない荒削りなプロジェクトテンプレートですが、よかったらご参考ください。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

さらに追記です。

先程アップしましたが、Parts.js ver 7.0.0 でWSH対応を復活させました。
https://github.com/standard-software/partsjs/tree/v7.0.0

これで、Deno/Node.js/GAS/WSH と、非常に多くのプラットホームで動作するライブラリになっています。

プロジェクトのコードを見てもらうと、Testコードを実装している方もビルドを通しているので、応用してもらうと、ライブラリ側のjs とアプリ側のjsとを区分けしてビルド環境を整えることもできるので、
WSH開発環境として、最新のJSを使えるので結構便利だと思います。

array.filter とか array.map とか、ご自身でPolyfillを組み込んでもありですが、Parts.js では、parts.array.filter とか parts.array.operation.filter とか part.array.map とか、一通りいわゆる最新のJSで使われるようなものは関数実装しているので、よかったらご利用ください。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.07%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る