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 +64 -0
- package/dist/ast2lua.js +254 -98
- package/dist/cli.js +43 -7
- package/dist/globalRename.js +35 -0
- package/dist/index.js +3 -19
- package/dist/keywordLocator.js +68 -0
- package/dist/linker.js +109 -0
- package/dist/minifier.js +248 -53
- package/dist/output.js +33 -0
- package/dist/renamer.js +101 -0
- package/dist/resolver.js +309 -0
- package/dist/transform.js +401 -0
- package/package.json +29 -15
- package/.eslintrc.json +0 -20
- package/src/ast2lua.ts +0 -812
- package/src/cli.ts +0 -56
- package/src/minifier.ts +0 -118
- package/tsconfig.json +0 -109
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
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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,
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
return this.generateIdentifier(nameItem, nested);
|
|
784
|
+
if (base.name !== "require" && base.name !== "dofile") {
|
|
785
|
+
return undefined;
|
|
673
786
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
fs_1.default.writeFileSync(
|
|
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);
|