storm-lua-minify 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,3 +13,67 @@ npx storm-lua-minify script.lua
13
13
  ```
14
14
 
15
15
  - `-m`オプションを付加すると、モジュールの挙動をLuaの実際の挙動に近づけます
16
+
17
+ # Source Map
18
+
19
+ このツールは、ミニファイ後のコードと合わせて [Source Map](https://tc39.es/source-map/) (`.lua.map`) を出力します。
20
+
21
+ ## sourceMappingURLアノテーションの出力形式
22
+
23
+ 出力される `.min.lua` の末尾に付く `sourceMappingURL` アノテーションの書式は、3種類から選べます。
24
+
25
+ | オプション | 書式 | 特徴 |
26
+ | ---------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
+ | (既定) | 複数行の `--[[`〜`]]` ブロックコメント | 過去のバージョンと同じ出力。既存ツールとの互換性を優先する場合はこのまま使う |
28
+ | `--single-line-source-mapping-url` | 単一行の `--` ラインコメント | 有効なLuaのまま、Source Map仕様が定める「アノテーションは生成コードの最終行に置く」という規則を満たす |
29
+ | `--strict-source-mapping-url` | Luaコメントで包まないマーカー文字列そのまま | 規則を厳密に満たすが、出力ファイルの最終行は有効なLua文ではなくなる(Lua文法上、`//`から始まる行をコメント化なしに構文解析可能にする方法は存在しないため) |
30
+
31
+ 「最終行に置く」規則を厳密な前提とするツール(生成コードの末尾だけを見て `sourceMappingURL` を解決する実装など)と組み合わせる場合は、`--single-line-source-mapping-url` を試してください。
32
+
33
+ ## sourcesContent
34
+
35
+ `.lua.map` の `sourcesContent` には、エントリファイルおよび `require`/`dofile` で参照される全モジュールの元テキストがそのまま埋め込まれます。ビューア側で元の `.lua` ファイルに個別にアクセスできなくても、Source Mapファイル単体で元コードを参照できます。
36
+
37
+ ## キーワードトークンのマッピング
38
+
39
+ 識別子・リテラル・式などASTノードに対応する出力は、それぞれ元ソース上の対応する位置に正確にマッピングされます。加えて、`then`/`elseif`/`else`/`end`/`do`/`until` といった、AST上では文・式の境界としてしか位置を持たないキーワードについても、それぞれが実際に出現する位置に個別にマッピングされます(例: `if`〜`then`〜`end` の `then` と `end` は、`if` とは別の、それぞれ自身の出現位置を持ちます)。トークン単位で対応関係を表示するビューアと組み合わせる際は、この粒度でのマッピングが利用されます。
40
+
41
+ ## 識別子短縮関連のオプション
42
+
43
+ 既定では以下がすべて有効です。デバッグ用途や意図しない出力になった場合は個別に無効化できます。
44
+
45
+ - `--no-rename`: すべての識別子短縮を無効にします
46
+ - `--no-merge-locals`: 連続する`local`変数宣言のまとめ上げを無効にします
47
+ - `--no-global-rename`: 代入されている(=スクリプト内部でのみ使われている)グローバル識別子の短縮を無効にします
48
+ - `--no-global-alias`: 短縮できない外部グローバル識別子(`screen`など、代入されず参照のみされるもの)を、頻出する場合にローカル変数へ代入して短縮する最適化を無効にします
49
+ - `--reserved-globals-config <path>`: `{"neverRenameGlobals": ["name", ...]}`形式のJSONファイルを指定し、**代入されていても常にリネームしないグローバル名**を列挙します
50
+
51
+ ### ⚠️ `--reserved-globals-config` について(重要)
52
+
53
+ `--no-global-rename`が有効な場合、このツールは「プログラム中のどこかで代入されているグローバル名」を自動的に内部専用とみなしてリネームします。これは通常のスクリプト内部の状態変数には安全ですが、**実行環境(エンジンなど)が特定の名前のグローバル関数・変数を探して呼び出す規約がある場合、その名前を誤ってリネームしてしまうと、静かに(エラーなく)呼び出されなくなるバグになります**。
54
+
55
+ そのような規約(例: Stormworksのマイクロコントローラーでエンジン側が呼び出す特定名のコールバック関数など)が対象のスクリプトにある場合は、必ず`--reserved-globals-config`でその名前を保護対象として指定してください。規約を把握していない場合は、安全のため`--no-global-rename`で本機能自体を無効にすることを推奨します。
56
+
57
+ # 開発・テスト
58
+
59
+ 開発はpnpmに変更になっています(v0.3.0リリース時点より)
60
+
61
+ ```
62
+ pnpm ci
63
+ pnpm run build
64
+ pnpm test
65
+ ```
66
+
67
+ # Lint / Format
68
+
69
+ ```
70
+ pnpm run lint # ESLint
71
+ pnpm run format:check # Prettierのフォーマットチェック
72
+ pnpm run format # Prettierでフォーマット
73
+ ```
74
+
75
+ `test/` 以下にスナップショット・ラウンドトリップパース・識別子衝突検知のテストがあります。
76
+ 既知バグ(#11, #12 など)の再現ケースは `test.todo` として登録されており、`npm test` は成功しますが、
77
+ 修正が入るまではそのテスト自体は失敗した状態のまま todo 扱いになります。
78
+
79
+ スナップショットを更新する場合は `UPDATE_SNAPSHOTS=1 npm test` を実行してください。
package/dist/ast2lua.js CHANGED
@@ -2,8 +2,12 @@
2
2
  // based on "luamin": Copyright Mathias Bynens <https://mathiasbynens.be/>
3
3
  // SPDX-License-Identifier: MIT
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.MinifyFile = void 0;
5
+ exports.MinifyFile = exports.IDENTIFIER_PARTS = void 0;
6
+ exports.isKeyword = isKeyword;
6
7
  const source_map_1 = require("source-map");
8
+ const linker_1 = require("./linker");
9
+ const keywordLocator_1 = require("./keywordLocator");
10
+ const transform_1 = require("./transform");
7
11
  const PRECEDENCE = {
8
12
  or: 1,
9
13
  and: 2,
@@ -13,28 +17,25 @@ const PRECEDENCE = {
13
17
  ">=": 3,
14
18
  "~=": 3,
15
19
  "==": 3,
16
- "..": 5,
17
- "+": 6,
18
- "-": 6, // binary -
19
- "*": 7,
20
- "/": 7,
21
- "%": 7,
22
- unarynot: 8,
23
- "unary#": 8,
24
- "unary-": 8, // unary -
25
- "^": 10,
20
+ "|": 4,
21
+ "~": 5, // binary ~ (bxor)
22
+ "&": 6,
23
+ "<<": 7,
24
+ ">>": 7,
25
+ "..": 9,
26
+ "+": 10,
27
+ "-": 10, // binary -
28
+ "*": 11,
29
+ "/": 11,
30
+ "//": 11,
31
+ "%": 11,
32
+ unarynot: 12,
33
+ "unary#": 12,
34
+ "unary-": 12, // unary -
35
+ "unary~": 12, // unary ~ (bnot)
36
+ "^": 14,
26
37
  };
27
- const IDENTIFIER_PARTS = [
28
- "0",
29
- "1",
30
- "2",
31
- "3",
32
- "4",
33
- "5",
34
- "6",
35
- "7",
36
- "8",
37
- "9",
38
+ exports.IDENTIFIER_PARTS = [
38
39
  "a",
39
40
  "b",
40
41
  "c",
@@ -95,6 +96,7 @@ function wrapArray(obj) {
95
96
  }
96
97
  return [obj];
97
98
  }
99
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- 現時点では未参照だが、オリジナル(luamin)由来のコードのため残置
98
100
  function generateZeroes(length) {
99
101
  let zero = "0";
100
102
  let result = "";
@@ -108,7 +110,6 @@ function generateZeroes(length) {
108
110
  if (length & 1) {
109
111
  result += zero;
110
112
  }
111
- // eslint-disable-next-line no-cond-assign
112
113
  if ((length >>= 1)) {
113
114
  zero += zero;
114
115
  }
@@ -206,11 +207,13 @@ function insertSeparator(a, b, separator = " ") {
206
207
  }
207
208
  class MinifyFile {
208
209
  fileName;
210
+ moduleName;
209
211
  ast;
210
212
  minifier;
211
213
  mode;
212
- constructor(fileName, ast, minifier, mode) {
214
+ constructor(fileName, moduleName, ast, minifier, mode) {
213
215
  this.fileName = fileName;
216
+ this.moduleName = moduleName;
214
217
  this.ast = ast;
215
218
  this.minifier = minifier;
216
219
  this.mode = mode;
@@ -219,7 +222,10 @@ class MinifyFile {
219
222
  const body = this.formatStatementList(this.ast.body);
220
223
  if (!noComment && this.ast.comments) {
221
224
  const comments = this.ast.comments;
222
- comments.reverse().filter(v => v.raw.includes("--#") || v.raw.includes("[[#")).forEach(comment => {
225
+ comments
226
+ .reverse()
227
+ .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
228
+ .forEach((comment) => {
223
229
  body.prepend([this.sourceNodeHelper(comment, comment.raw), "\n"]);
224
230
  });
225
231
  return body;
@@ -228,11 +234,83 @@ class MinifyFile {
228
234
  return body;
229
235
  }
230
236
  }
237
+ /**
238
+ * モジュール本体の最後の文が「単一の式を返すreturn文」であるときに限り、
239
+ * それ以外の文と最後の式を分けて返す。requireを式(IIFE)ではなく文として
240
+ * 展開したい呼び出し側(#29のレビュー対応)が利用する。
241
+ * 該当しない場合はundefinedを返し、呼び出し側はIIFE方式へフォールバックする。
242
+ */
243
+ parseAsStatementsAndFinalExpression(noComment) {
244
+ const body = this.ast.body;
245
+ const last = body[body.length - 1];
246
+ if (!last ||
247
+ last.type !== "ReturnStatement" ||
248
+ last.arguments.length !== 1) {
249
+ return undefined;
250
+ }
251
+ const statements = this.formatStatementList(body.slice(0, -1));
252
+ if (!noComment && this.ast.comments) {
253
+ this.ast.comments
254
+ .slice()
255
+ .reverse()
256
+ .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
257
+ .forEach((comment) => {
258
+ statements.prepend([
259
+ this.sourceNodeHelper(comment, comment.raw),
260
+ "\n",
261
+ ]);
262
+ });
263
+ }
264
+ const finalExpression = this.formatExpression(last.arguments[0]);
265
+ return { statements, finalExpression };
266
+ }
231
267
  sourceNodeHelper(node, chuncks, name) {
232
268
  const line = node?.loc?.start.line;
233
269
  const column = node?.loc?.start.column;
234
- return new source_map_1.SourceNode(line == undefined ? null : line, column == undefined ? null : column, this.fileName, // 本当に自分のファイル名でよいかは要検討
235
- chuncks, name);
270
+ // this.fileNameは常にこのMinifyFileインスタンスが担当するモジュール自身の
271
+ // ファイル名(Linkパスで解決済み)なので、ここで出力するノードの由来ファイルとして正しい。
272
+ return new source_map_1.SourceNode(line == undefined ? null : line, column == undefined ? null : column, this.fileName, chuncks, name);
273
+ }
274
+ _keywordLocator;
275
+ /**
276
+ * luaparseのASTはキーワード単体の位置を持たないため、`then`/`do`/`until`の
277
+ * ような「文・式の間に挟まるキーワード」の正確な位置は、モジュールの
278
+ * ソースを再トークン化して求める(#14)。初回アクセス時に1回だけ構築する。
279
+ */
280
+ keywordLocator() {
281
+ if (!this._keywordLocator) {
282
+ const sourceText = this.minifier.moduleSourceText.get(this.moduleName);
283
+ if (sourceText == undefined) {
284
+ throw new Error(this.moduleName + " is not found");
285
+ }
286
+ this._keywordLocator = new keywordLocator_1.KeywordLocator(sourceText, this.minifier.luaParseSettings);
287
+ }
288
+ return this._keywordLocator;
289
+ }
290
+ /**
291
+ * `anchor`ノードの直後(`anchor.loc.end`以降)で最初に現れる`value`という
292
+ * 値のキーワードトークンを探し、その位置を持つ`SourceNode`を返す。
293
+ * `anchor`は「探しているキーワードの直前に必ず存在する、既に位置が分かって
294
+ * いるノード」(例: `then`の場合は条件式)を渡す。ASTの入れ子構造上、
295
+ * その直前ノードの`loc.end`以降を走査すれば、ネストした同名キーワード
296
+ * (入れ子の`if...then`等)を誤って拾うことはない。
297
+ */
298
+ keywordAfter(anchor, value) {
299
+ return this.keywordFrom(anchor.loc?.end, value);
300
+ }
301
+ keywordFrom(from, value) {
302
+ const pos = from ? this.keywordLocator().findFrom(from, value) : undefined;
303
+ return new source_map_1.SourceNode(pos?.line ?? null, pos?.column ?? null, this.fileName, value);
304
+ }
305
+ /**
306
+ * `if`/`while`/`do`/`for`/`function`を締める`end`キーワードの位置を返す。
307
+ * これらの文は必ず`end`で終わり、末尾に余計な内容が続かないため、
308
+ * 文全体の`loc.end`(luaparseが既に計算済み)から`end`の文字数(3)を
309
+ * 引くだけで、再トークン化なしに正確な位置を求められる。
310
+ */
311
+ endKeyword(node) {
312
+ const end = node.loc?.end;
313
+ return new source_map_1.SourceNode(end?.line ?? null, end == undefined ? null : end.column - 3, this.fileName, "end");
236
314
  }
237
315
  formatStatementList(body) {
238
316
  const result = this.sourceNodeHelper(undefined, []);
@@ -241,7 +319,66 @@ class MinifyFile {
241
319
  });
242
320
  return result;
243
321
  }
322
+ /**
323
+ * SLモード限定: `local x = require("m")` / `x = require("m")` の形(変数1個・
324
+ * 初期化式1個)を、requireを式(IIFE)として埋め込むのではなく、モジュール本体の
325
+ * 文をそのまま展開し最後にターゲットへ代入する「文」の並びとして出力する
326
+ * (#29のレビュー対応。無オプション実行時の挙動互換性のため)。
327
+ *
328
+ * モジュール本体が「単一の式を返すreturn文」で終わっていない場合や、変数・
329
+ * 初期化式が複数ある場合、-mモードの場合は対象外とし、undefinedを返す
330
+ * (呼び出し側は従来のIIFE方式にフォールバックする)。
331
+ */
332
+ trySpliceRequireStatement(statement) {
333
+ if (this.mode.moduleLikeLua) {
334
+ return undefined;
335
+ }
336
+ if (statement.variables.length !== 1 || statement.init.length !== 1) {
337
+ return undefined;
338
+ }
339
+ const moduleRef = this.matchModuleCallExpression(statement.init[0]);
340
+ if (!moduleRef || moduleRef.kind !== "require") {
341
+ return undefined;
342
+ }
343
+ const spliced = this.minifier.splitModuleForStatementSplice(moduleRef.moduleName);
344
+ if (!spliced) {
345
+ return undefined;
346
+ }
347
+ const isLocal = statement.type === "LocalStatement";
348
+ const target = statement.variables[0];
349
+ const targetNode = isLocal
350
+ ? this.generateIdentifier(target)
351
+ : this.formatExpression(target);
352
+ const result = this.sourceNodeHelper(statement, []);
353
+ addWithSeparator(result, spliced.statements);
354
+ addWithSeparator(result, isLocal ? ["local ", targetNode] : [targetNode]);
355
+ addWithSeparator(result, "=");
356
+ addWithSeparator(result, spliced.finalExpression);
357
+ return result;
358
+ }
359
+ /**
360
+ * SLモード限定: `require("m")` を戻り値を使わない単独の文として書いた場合、
361
+ * dofileと同じくfunctionで包まずそのまま展開する(Cプリプロセッサのincludeに
362
+ * 近い、LifeBoat Modeでの一般的な使い方に合わせるための対応。#29のレビュー対応)。
363
+ */
364
+ tryInlineBareRequireStatement(expr) {
365
+ if (this.mode.moduleLikeLua) {
366
+ return undefined;
367
+ }
368
+ const moduleRef = this.matchModuleCallExpression(expr);
369
+ if (!moduleRef || moduleRef.kind !== "require") {
370
+ return undefined;
371
+ }
372
+ return this.minifier.printModuleInline(moduleRef.moduleName);
373
+ }
244
374
  formatStatement(statement) {
375
+ if (statement.type == "AssignmentStatement" ||
376
+ statement.type == "LocalStatement") {
377
+ const spliced = this.trySpliceRequireStatement(statement);
378
+ if (spliced) {
379
+ return spliced;
380
+ }
381
+ }
245
382
  if (statement.type == "AssignmentStatement") {
246
383
  // left-hand side
247
384
  const variables = statement.variables
@@ -273,6 +410,10 @@ class MinifyFile {
273
410
  return result;
274
411
  }
275
412
  else if (statement.type == "CallStatement") {
413
+ const bareRequire = this.tryInlineBareRequireStatement(statement.expression);
414
+ if (bareRequire) {
415
+ return bareRequire;
416
+ }
276
417
  return this.formatExpression(statement.expression); // NOTE: もう一度囲んでもいい
277
418
  }
278
419
  else if (statement.type == "IfStatement") {
@@ -282,12 +423,12 @@ class MinifyFile {
282
423
  if (clause.type == "IfClause") {
283
424
  addWithSeparator(clauseMap, "if");
284
425
  addWithSeparator(clauseMap, this.formatExpression(clause.condition));
285
- addWithSeparator(clauseMap, "then");
426
+ addWithSeparator(clauseMap, this.keywordAfter(clause.condition, "then"));
286
427
  }
287
428
  else if (clause.type == "ElseifClause") {
288
429
  addWithSeparator(clauseMap, "elseif");
289
430
  addWithSeparator(clauseMap, this.formatExpression(clause.condition));
290
- addWithSeparator(clauseMap, "then");
431
+ addWithSeparator(clauseMap, this.keywordAfter(clause.condition, "then"));
291
432
  }
292
433
  else {
293
434
  addWithSeparator(clauseMap, "else");
@@ -295,21 +436,21 @@ class MinifyFile {
295
436
  addWithSeparator(clauseMap, this.formatStatementList(clause.body));
296
437
  addWithSeparator(result, clauseMap);
297
438
  });
298
- addWithSeparator(result, "end");
439
+ addWithSeparator(result, this.endKeyword(statement));
299
440
  return result;
300
441
  }
301
442
  else if (statement.type == "WhileStatement") {
302
443
  const result = this.sourceNodeHelper(statement, "while");
303
444
  addWithSeparator(result, this.formatExpression(statement.condition));
304
- addWithSeparator(result, "do");
445
+ addWithSeparator(result, this.keywordAfter(statement.condition, "do"));
305
446
  addWithSeparator(result, this.formatStatementList(statement.body));
306
- addWithSeparator(result, "end");
447
+ addWithSeparator(result, this.endKeyword(statement));
307
448
  return result;
308
449
  }
309
450
  else if (statement.type == "DoStatement") {
310
451
  const result = this.sourceNodeHelper(statement, "do");
311
452
  addWithSeparator(result, this.formatStatementList(statement.body));
312
- addWithSeparator(result, "end");
453
+ addWithSeparator(result, this.endKeyword(statement));
313
454
  return result;
314
455
  }
315
456
  else if (statement.type == "ReturnStatement") {
@@ -328,7 +469,13 @@ class MinifyFile {
328
469
  else if (statement.type == "RepeatStatement") {
329
470
  const result = this.sourceNodeHelper(statement, "repeat");
330
471
  addWithSeparator(result, this.formatStatementList(statement.body));
331
- addWithSeparator(result, "until");
472
+ // body内の最後の文があればその直後、無ければ`repeat`キーワード自身の
473
+ // 開始位置から`until`を探す(bodyが空でも、その手前には`repeat`しか
474
+ // 存在しないため安全に検索できる)。
475
+ const untilAnchor = statement.body.length
476
+ ? statement.body[statement.body.length - 1].loc?.end
477
+ : statement.loc?.start;
478
+ addWithSeparator(result, this.keywordFrom(untilAnchor, "until"));
332
479
  addWithSeparator(result, this.formatExpression(statement.condition));
333
480
  return result;
334
481
  }
@@ -353,7 +500,7 @@ class MinifyFile {
353
500
  }
354
501
  addWithSeparator(result, ")");
355
502
  addWithSeparator(result, this.formatStatementList(statement.body));
356
- addWithSeparator(result, "end");
503
+ addWithSeparator(result, this.endKeyword(statement));
357
504
  return result;
358
505
  }
359
506
  else if (statement.type == "ForGenericStatement") {
@@ -368,9 +515,9 @@ class MinifyFile {
368
515
  addWithSeparator(result, variables.slice(0, -1));
369
516
  addWithSeparator(result, "in");
370
517
  addWithSeparator(result, iterators.slice(0, -1));
371
- addWithSeparator(result, "do");
518
+ addWithSeparator(result, this.keywordAfter(statement.iterators[statement.iterators.length - 1], "do"));
372
519
  addWithSeparator(result, this.formatStatementList(statement.body));
373
- addWithSeparator(result, "end");
520
+ addWithSeparator(result, this.endKeyword(statement));
374
521
  return result;
375
522
  }
376
523
  else if (statement.type == "ForNumericStatement") {
@@ -385,9 +532,9 @@ class MinifyFile {
385
532
  addWithSeparator(result, ",");
386
533
  addWithSeparator(result, this.formatExpression(statement.step));
387
534
  }
388
- addWithSeparator(result, "do");
535
+ addWithSeparator(result, this.keywordAfter(statement.step ?? statement.end, "do"));
389
536
  addWithSeparator(result, this.formatStatementList(statement.body));
390
- addWithSeparator(result, "end");
537
+ addWithSeparator(result, this.endKeyword(statement));
391
538
  return result;
392
539
  }
393
540
  else if (statement.type == "LabelStatement") {
@@ -414,13 +561,7 @@ class MinifyFile {
414
561
  }*/
415
562
  formatExpression(expression, argOptions) {
416
563
  if (expression.type == "Identifier") {
417
- return this.sourceNodeHelper(expression,
418
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
419
- //@ts-ignore
420
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
421
- expression.isLocal
422
- ? this.generateIdentifier(expression, true)
423
- : expression.name, expression.name);
564
+ return this.generateIdentifier(expression);
424
565
  }
425
566
  else if (expression.type == "StringLiteral" ||
426
567
  expression.type == "NumericLiteral" ||
@@ -500,22 +641,11 @@ class MinifyFile {
500
641
  return result;
501
642
  }
502
643
  else if (expression.type == "CallExpression") {
503
- // requireの展開モードは2種類: SLモード-その場に読み込み, FLモード-require相当の関数で呼び出し
504
- const callExpr = this.formatBase(expression.base).toString();
505
- if ((callExpr === "require" || callExpr === "dofile") &&
506
- expression?.arguments[0].type === "StringLiteral") {
507
- const moduleName = expression.arguments[0].raw
508
- .replaceAll('"', "")
509
- .replaceAll("'", "");
510
- const res = this.minifier.parseModule(moduleName);
511
- if (this.mode.moduleLikeLua) {
512
- if (callExpr === "dofile") {
513
- return (res || this.sourceNodeHelper(undefined, ""));
514
- }
515
- // requireならスキップ
516
- }
517
- else {
518
- return (res || this.sourceNodeHelper(undefined, ""));
644
+ const moduleRef = this.matchModuleCallExpression(expression);
645
+ if (moduleRef) {
646
+ const replacement = this.formatModuleReference(expression, moduleRef);
647
+ if (replacement) {
648
+ return replacement;
519
649
  }
520
650
  }
521
651
  const args = expression.arguments
@@ -535,6 +665,13 @@ class MinifyFile {
535
665
  ]);
536
666
  }
537
667
  else if (expression.type == "StringCallExpression") {
668
+ const moduleRef = this.matchModuleCallExpression(expression);
669
+ if (moduleRef) {
670
+ const replacement = this.formatModuleReference(expression, moduleRef);
671
+ if (replacement) {
672
+ return replacement;
673
+ }
674
+ }
538
675
  return this.sourceNodeHelper(expression, [
539
676
  this.formatExpression(expression.base),
540
677
  this.formatExpression(expression.argument),
@@ -575,7 +712,7 @@ class MinifyFile {
575
712
  result.add(")");
576
713
  const body = this.formatStatementList(expression.body);
577
714
  addWithSeparator(result, body);
578
- addWithSeparator(result, "end");
715
+ addWithSeparator(result, this.endKeyword(expression));
579
716
  return result;
580
717
  }
581
718
  else if (expression.type == "TableConstructorExpression") {
@@ -637,43 +774,62 @@ class MinifyFile {
637
774
  }
638
775
  return result;
639
776
  }
640
- currentIdentifier = "";
641
- generateIdentifier(nameItem, nested = false) {
642
- if (nameItem.name === "self") {
643
- return this.sourceNodeHelper(nameItem, "self", "self");
644
- }
645
- const defined = this.minifier.identifierMap.get(nameItem.name);
646
- if (defined) {
647
- return this.sourceNodeHelper(nameItem, defined, nameItem.name); // 第3引数は要調査
648
- }
649
- const length = this.currentIdentifier.length;
650
- let position = length - 1;
651
- let character;
652
- let index;
653
- while (position >= 0) {
654
- character = this.currentIdentifier.charAt(position);
655
- index = IDENTIFIER_PARTS.indexOf(character);
656
- if (index != IDENTIFIER_PARTS.length - 1) {
657
- this.currentIdentifier =
658
- this.currentIdentifier.substring(0, position) +
659
- IDENTIFIER_PARTS[index + 1] +
660
- generateZeroes(length - (position + 1));
661
- if (isKeyword(this.currentIdentifier) ||
662
- this.minifier.identifiersInUse.has(this.currentIdentifier)) {
663
- return this.generateIdentifier(nameItem, nested);
664
- }
665
- this.minifier.identifierMap.set(nameItem.name, this.currentIdentifier);
666
- return this.generateIdentifier(nameItem, nested);
667
- }
668
- --position;
777
+ // require/dofileへの静的な呼び出しを検出する。実際のモジュール解決はLinkパス
778
+ // (Minifier.link)が事前に済ませているため、ここでは呼び出し形が
779
+ // require/dofile への静的文字列呼び出しかどうかを判定するだけでよい(#18)。
780
+ matchModuleCall(base, argument) {
781
+ if (base.type !== "Identifier") {
782
+ return undefined;
669
783
  }
670
- this.currentIdentifier = "a" + generateZeroes(length);
671
- if (this.minifier.identifiersInUse.has(this.currentIdentifier)) {
672
- return this.generateIdentifier(nameItem, nested);
784
+ if (base.name !== "require" && base.name !== "dofile") {
785
+ return undefined;
673
786
  }
674
- this.minifier.identifierMap.set(nameItem.name, this.currentIdentifier);
675
- return this.generateIdentifier(nameItem, nested);
676
- // return this.sourceNodeHelper(nameItem, nameItem.name, nested ? nameItem.name : undefined);
787
+ const moduleName = (0, linker_1.staticStringArgument)(argument);
788
+ if (moduleName === undefined) {
789
+ return undefined;
790
+ }
791
+ return { kind: base.name, moduleName };
792
+ }
793
+ // CallExpression / StringCallExpression のどちらの構文でも呼べるmatchModuleCall
794
+ matchModuleCallExpression(expr) {
795
+ if (expr.type === "CallExpression") {
796
+ return this.matchModuleCall(expr.base, expr.arguments[0]);
797
+ }
798
+ if (expr.type === "StringCallExpression") {
799
+ return this.matchModuleCall(expr.base, expr.argument);
800
+ }
801
+ return undefined;
802
+ }
803
+ formatModuleReference(expression, ref) {
804
+ if (ref.kind === "dofile") {
805
+ // dofileは呼び出しごとに毎回展開しなおす(キャッシュしない)
806
+ return this.minifier.printModuleInline(ref.moduleName);
807
+ }
808
+ if (!this.mode.moduleLikeLua) {
809
+ // SLモード(無オプション): requireもキャッシュせずその場展開する
810
+ // (挙動互換性のため、ホイストした共有ローカルへの参照にはしない)。
811
+ // 式の位置に置けるようにIIFEで包む。
812
+ const body = this.minifier.printModuleInline(ref.moduleName);
813
+ return this.sourceNodeHelper(expression, [
814
+ "(function() ",
815
+ body,
816
+ " end)()",
817
+ ]);
818
+ }
819
+ // -mモードのrequireはそのまま呼び出しとして出力し、実行時のrequire関数に委ねる
820
+ return undefined;
821
+ }
822
+ // Renameパス(#20)が解決済みシンボルテーブルをもとに割り当てた短縮名を参照する。
823
+ // 対応するローカルシンボルが無い場合(グローバル参照や"self")は元の名前のまま出力する。
824
+ generateIdentifier(nameItem) {
825
+ const renamed = this.minifier
826
+ .getRenameResult(this.moduleName)
827
+ .nameOf(nameItem);
828
+ // #8bのエイリアス化はノードの`.name`自体を書き換えるため、loc(元のソース上の
829
+ // 位置)はそのままでもnameItem.nameはもう元の識別子名ではない。Source Mapの
830
+ // namesフィールドには常に「元のソースで書かれていた名前」を載せる必要がある。
831
+ const originalName = (0, transform_1.originalNameOf)(nameItem) ?? nameItem.name;
832
+ return this.sourceNodeHelper(nameItem, renamed ?? nameItem.name, originalName);
677
833
  }
678
834
  }
679
835
  exports.MinifyFile = MinifyFile;
package/dist/cli.js CHANGED
@@ -8,11 +8,19 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const commander_1 = require("commander");
10
10
  const minifier_1 = require("./minifier");
11
+ const output_1 = require("./output");
11
12
  const program = new commander_1.Command();
12
13
  program
13
- .version("0.1.3")
14
+ .version("0.3.0")
14
15
  .description("A Lua minifier also outputs source map")
15
- .option("-m, --module-like-lua", "require・dofileの動作を実際のLuaに近づけます");
16
+ .option("-m, --module-like-lua", "require・dofileの動作を実際のLuaに近づけます")
17
+ .option("--no-rename", "識別子の短縮(リネーム)を無効にします(デバッグ用途)")
18
+ .option("--no-global-rename", "内部でのみ使用するグローバル識別子の短縮を無効にします(デバッグ用途)")
19
+ .option("--no-merge-locals", "連続するローカル変数宣言のまとめ上げを無効にします(デバッグ用途)")
20
+ .option("--no-global-alias", "外部グローバル識別子(リネームできないもの)のローカル代入短縮を無効にします(デバッグ用途)")
21
+ .option("--reserved-globals-config <path>", '代入されていても短縮しないグローバル名を列挙したJSON設定ファイルのパス({"neverRenameGlobals":["onTick",...]}形式)。エンジン側のコールバック規約名など、常に元の名前のまま残す必要がある識別子を指定します')
22
+ .option("--single-line-source-mapping-url", "sourceMappingURLアノテーションを単一行の--コメントで出力します(Source Map仕様の「最終行」ルールに従いますが、既定の複数行ブロックコメント形式を前提とするツールとは組み合わせられません)")
23
+ .option("--strict-source-mapping-url", "sourceMappingURLアノテーションをLuaコメントで一切包まず、Source Map仕様のマーカー文字列(//# sourceMappingURL=...)そのままを出力します。Luaの文法上この形式と有効なLuaコードは両立できないため、出力ファイルの最終行は有効なLua文ではなくなります");
16
24
  program.parse(process.argv);
17
25
  const luaFiles = program.args;
18
26
  const luaparseSetting = {
@@ -21,7 +29,36 @@ const luaparseSetting = {
21
29
  ranges: true,
22
30
  scope: true,
23
31
  };
24
- const mode = program.opts();
32
+ function isReservedGlobalsConfig(value) {
33
+ if (typeof value !== "object" || value === null) {
34
+ return false;
35
+ }
36
+ const candidate = value;
37
+ return (Array.isArray(candidate.neverRenameGlobals) &&
38
+ candidate.neverRenameGlobals.every((name) => typeof name === "string"));
39
+ }
40
+ function loadNeverRenameGlobals(configPath) {
41
+ if (!fs_1.default.existsSync(configPath)) {
42
+ throw new Error("Reserved globals config not found: " + configPath);
43
+ }
44
+ const parsed = JSON.parse(fs_1.default.readFileSync(configPath).toString());
45
+ if (!isReservedGlobalsConfig(parsed)) {
46
+ throw new Error(configPath +
47
+ ' must be a JSON object of the form {"neverRenameGlobals": ["name", ...]}');
48
+ }
49
+ return new Set(parsed.neverRenameGlobals);
50
+ }
51
+ const { singleLineSourceMappingUrl, strictSourceMappingUrl, reservedGlobalsConfig, ...mode } = program.opts();
52
+ if (reservedGlobalsConfig) {
53
+ mode.neverRenameGlobals = loadNeverRenameGlobals(reservedGlobalsConfig);
54
+ }
55
+ // 既定は旧バージョンと互換の複数行ブロックコメント("legacy")。
56
+ // --strict-source-mapping-url > --single-line-source-mapping-url の優先順で上書きする。
57
+ const sourceMappingUrlStyle = strictSourceMappingUrl
58
+ ? "strict"
59
+ : singleLineSourceMappingUrl
60
+ ? "line"
61
+ : "legacy";
25
62
  luaFiles.forEach((fileName) => {
26
63
  const parsedFileName = path_1.default.parse(fileName);
27
64
  if (fs_1.default.existsSync(fileName)) {
@@ -36,10 +73,9 @@ luaFiles.forEach((fileName) => {
36
73
  name: parsedFileName.name,
37
74
  ext: parsedFileName.ext + ".map",
38
75
  });
39
- map.add("\n--[[\n//# sourceMappingURL=" + path_1.default.basename(mapFileName) + "\n]]");
40
- const sourceAndMap = map.toStringWithSourceMap();
41
- fs_1.default.writeFileSync(minFileName, sourceAndMap.code);
42
- fs_1.default.writeFileSync(mapFileName, JSON.stringify(sourceAndMap.map));
76
+ const { code, map: mapJson } = (0, output_1.buildMinifiedOutput)(map, minFileName, mapFileName, { sourceMappingUrlStyle });
77
+ fs_1.default.writeFileSync(minFileName, code);
78
+ fs_1.default.writeFileSync(mapFileName, mapJson);
43
79
  }
44
80
  else {
45
81
  console.error("No such file: " + fileName);