storm-lua-minify 0.1.2 → 0.2.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.
Files changed (81) hide show
  1. package/.github/workflows/ci.yml +39 -0
  2. package/.github/workflows/publish.yml +39 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc.json +1 -0
  5. package/LICENSE +21 -21
  6. package/README.md +37 -15
  7. package/dist/ast2lua.js +188 -85
  8. package/dist/cli.js +17 -7
  9. package/dist/index.js +3 -19
  10. package/dist/linker.js +96 -0
  11. package/dist/minifier.js +176 -51
  12. package/dist/output.js +33 -0
  13. package/dist/renamer.js +87 -0
  14. package/dist/resolver.js +303 -0
  15. package/eslint.config.js +29 -0
  16. package/package.json +33 -27
  17. package/src/ast2lua.ts +940 -812
  18. package/src/cli.ts +86 -56
  19. package/src/linker.ts +108 -0
  20. package/src/minifier.ts +284 -114
  21. package/src/output.ts +71 -0
  22. package/src/renamer.ts +134 -0
  23. package/src/resolver.ts +378 -0
  24. package/test/circular-require.test.ts +19 -0
  25. package/test/fixtures/bare-require/main.lua +2 -0
  26. package/test/fixtures/bare-require/mod.lua +1 -0
  27. package/test/fixtures/bitwise-precedence/main.lua +10 -0
  28. package/test/fixtures/circular-require/a.lua +2 -0
  29. package/test/fixtures/circular-require/b.lua +2 -0
  30. package/test/fixtures/circular-require/main.lua +2 -0
  31. package/test/fixtures/dofile/greet.lua +1 -0
  32. package/test/fixtures/dofile/main.lua +2 -0
  33. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +2 -0
  34. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +2 -0
  35. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +2 -0
  36. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +2 -0
  37. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +2 -0
  38. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +2 -0
  39. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +2 -0
  40. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +2 -0
  41. package/test/fixtures/entry-scope-many-requires/dep_india.lua +2 -0
  42. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +2 -0
  43. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +2 -0
  44. package/test/fixtures/entry-scope-many-requires/main.lua +25 -0
  45. package/test/fixtures/multi-require/common.lua +1 -0
  46. package/test/fixtures/multi-require/main.lua +3 -0
  47. package/test/fixtures/nested-module/main.lua +2 -0
  48. package/test/fixtures/nested-module/sub/deep.lua +1 -0
  49. package/test/fixtures/require-call/main.lua +2 -0
  50. package/test/fixtures/require-call/mod.lua +5 -0
  51. package/test/fixtures/require-in-expression/main.lua +1 -0
  52. package/test/fixtures/require-in-expression/mod.lua +1 -0
  53. package/test/fixtures/require-string-call/main.lua +2 -0
  54. package/test/fixtures/require-string-call/mod.lua +5 -0
  55. package/test/fixtures/single-file/main.lua +15 -0
  56. package/test/identifier-collision.test.ts +28 -0
  57. package/test/lib/collision.ts +131 -0
  58. package/test/lib/helpers.ts +124 -0
  59. package/test/no-rename.test.ts +19 -0
  60. package/test/output.test.ts +95 -0
  61. package/test/precedence.test.ts +28 -0
  62. package/test/renamer.test.ts +130 -0
  63. package/test/resolver.test.ts +206 -0
  64. package/test/roundtrip.test.ts +26 -0
  65. package/test/snapshot.test.ts +27 -0
  66. package/test/snapshots/bare-require.sl.lua +1 -0
  67. package/test/snapshots/bitwise-precedence.sl.lua +2 -0
  68. package/test/snapshots/dofile.sl.lua +1 -0
  69. package/test/snapshots/entry-scope-many-requires.m.lua +14 -0
  70. package/test/snapshots/multi-require.m.lua +4 -0
  71. package/test/snapshots/multi-require.sl.lua +1 -0
  72. package/test/snapshots/nested-module.m.lua +4 -0
  73. package/test/snapshots/require-call.m.lua +5 -0
  74. package/test/snapshots/require-call.sl.lua +1 -0
  75. package/test/snapshots/require-in-expression.sl.lua +1 -0
  76. package/test/snapshots/require-string-call.m.lua +5 -0
  77. package/test/snapshots/single-file.sl.lua +6 -0
  78. package/test/sourcemap.test.ts +105 -0
  79. package/tsconfig.eslint.json +8 -0
  80. package/tsconfig.json +111 -109
  81. package/.eslintrc.json +0 -20
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - develop
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - develop
12
+
13
+ jobs:
14
+ build-lint-test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: "22.x"
24
+ cache: "npm"
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Build
30
+ run: npm run build
31
+
32
+ - name: Lint
33
+ run: npm run lint
34
+
35
+ - name: Check formatting
36
+ run: npm run format:check
37
+
38
+ - name: Test
39
+ run: npm test
@@ -0,0 +1,39 @@
1
+ name: Publish to npm
2
+
3
+ # NOTE: npm publish is disabled for now (see issue #10).
4
+ # Once the repository is ready to publish (NPM_TOKEN secret configured,
5
+ # owner sign-off), uncomment the "Publish" step below.
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: "22.x"
21
+ cache: "npm"
22
+ registry-url: "https://registry.npmjs.org"
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Build
28
+ run: npm run build
29
+
30
+ - name: Lint
31
+ run: npm run lint
32
+
33
+ - name: Test
34
+ run: npm test
35
+
36
+ # - name: Publish
37
+ # run: npm publish
38
+ # env:
39
+ # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,3 @@
1
+ dist
2
+ node_modules
3
+ package-lock.json
@@ -0,0 +1 @@
1
+ {}
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Nona Takahara, Mathias Bynens
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nona Takahara, Mathias Bynens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,15 +1,37 @@
1
- # storm-lua-minify
2
-
3
- このプログラムは [mathiasbynens/luamin](https://github.com/mathiasbynens/luamin) をベースにした Stormworks: Build and Rescue 向けのLua minifierです。
4
-
5
- Stormworks: Build and Rescue 以外の用途にも使用可能です。
6
-
7
- # 使い方
8
-
9
- ```
10
- npm i storm-lua-minify
11
-
12
- npx storm-lua-minify script.lua
13
- ```
14
-
15
- - `-m`オプションを付加すると、モジュールの挙動をLuaの実際の挙動に近づけます
1
+ # storm-lua-minify
2
+
3
+ このプログラムは [mathiasbynens/luamin](https://github.com/mathiasbynens/luamin) をベースにした Stormworks: Build and Rescue 向けのLua minifierです。
4
+
5
+ Stormworks: Build and Rescue 以外の用途にも使用可能です。
6
+
7
+ # 使い方
8
+
9
+ ```
10
+ npm i storm-lua-minify
11
+
12
+ npx storm-lua-minify script.lua
13
+ ```
14
+
15
+ - `-m`オプションを付加すると、モジュールの挙動をLuaの実際の挙動に近づけます
16
+
17
+ # テスト
18
+
19
+ ```
20
+ npm ci
21
+ npm run build
22
+ npm test
23
+ ```
24
+
25
+ # Lint / Format
26
+
27
+ ```
28
+ npm run lint # ESLint
29
+ npm run format:check # Prettierのフォーマットチェック
30
+ npm run format # Prettierでフォーマット
31
+ ```
32
+
33
+ `test/` 以下にスナップショット・ラウンドトリップパース・識別子衝突検知のテストがあります。
34
+ 既知バグ(#11, #12 など)の再現ケースは `test.todo` として登録されており、`npm test` は成功しますが、
35
+ 修正が入るまではそのテスト自体は失敗した状態のまま todo 扱いになります。
36
+
37
+ スナップショットを更新する場合は `UPDATE_SNAPSHOTS=1 npm test` を実行してください。
package/dist/ast2lua.js CHANGED
@@ -2,8 +2,10 @@
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");
7
9
  const PRECEDENCE = {
8
10
  or: 1,
9
11
  and: 2,
@@ -13,28 +15,25 @@ const PRECEDENCE = {
13
15
  ">=": 3,
14
16
  "~=": 3,
15
17
  "==": 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,
18
+ "|": 4,
19
+ "~": 5, // binary ~ (bxor)
20
+ "&": 6,
21
+ "<<": 7,
22
+ ">>": 7,
23
+ "..": 9,
24
+ "+": 10,
25
+ "-": 10, // binary -
26
+ "*": 11,
27
+ "/": 11,
28
+ "//": 11,
29
+ "%": 11,
30
+ unarynot: 12,
31
+ "unary#": 12,
32
+ "unary-": 12, // unary -
33
+ "unary~": 12, // unary ~ (bnot)
34
+ "^": 14,
26
35
  };
27
- const IDENTIFIER_PARTS = [
28
- "0",
29
- "1",
30
- "2",
31
- "3",
32
- "4",
33
- "5",
34
- "6",
35
- "7",
36
- "8",
37
- "9",
36
+ exports.IDENTIFIER_PARTS = [
38
37
  "a",
39
38
  "b",
40
39
  "c",
@@ -95,6 +94,7 @@ function wrapArray(obj) {
95
94
  }
96
95
  return [obj];
97
96
  }
97
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- 現時点では未参照だが、オリジナル(luamin)由来のコードのため残置
98
98
  function generateZeroes(length) {
99
99
  let zero = "0";
100
100
  let result = "";
@@ -108,7 +108,6 @@ function generateZeroes(length) {
108
108
  if (length & 1) {
109
109
  result += zero;
110
110
  }
111
- // eslint-disable-next-line no-cond-assign
112
111
  if ((length >>= 1)) {
113
112
  zero += zero;
114
113
  }
@@ -206,11 +205,13 @@ function insertSeparator(a, b, separator = " ") {
206
205
  }
207
206
  class MinifyFile {
208
207
  fileName;
208
+ moduleName;
209
209
  ast;
210
210
  minifier;
211
211
  mode;
212
- constructor(fileName, ast, minifier, mode) {
212
+ constructor(fileName, moduleName, ast, minifier, mode) {
213
213
  this.fileName = fileName;
214
+ this.moduleName = moduleName;
214
215
  this.ast = ast;
215
216
  this.minifier = minifier;
216
217
  this.mode = mode;
@@ -219,7 +220,10 @@ class MinifyFile {
219
220
  const body = this.formatStatementList(this.ast.body);
220
221
  if (!noComment && this.ast.comments) {
221
222
  const comments = this.ast.comments;
222
- comments.reverse().filter(v => v.raw.includes("--#") || v.raw.includes("[[#")).forEach(comment => {
223
+ comments
224
+ .reverse()
225
+ .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
226
+ .forEach((comment) => {
223
227
  body.prepend([this.sourceNodeHelper(comment, comment.raw), "\n"]);
224
228
  });
225
229
  return body;
@@ -228,11 +232,42 @@ class MinifyFile {
228
232
  return body;
229
233
  }
230
234
  }
235
+ /**
236
+ * モジュール本体の最後の文が「単一の式を返すreturn文」であるときに限り、
237
+ * それ以外の文と最後の式を分けて返す。requireを式(IIFE)ではなく文として
238
+ * 展開したい呼び出し側(#29のレビュー対応)が利用する。
239
+ * 該当しない場合はundefinedを返し、呼び出し側はIIFE方式へフォールバックする。
240
+ */
241
+ parseAsStatementsAndFinalExpression(noComment) {
242
+ const body = this.ast.body;
243
+ const last = body[body.length - 1];
244
+ if (!last ||
245
+ last.type !== "ReturnStatement" ||
246
+ last.arguments.length !== 1) {
247
+ return undefined;
248
+ }
249
+ const statements = this.formatStatementList(body.slice(0, -1));
250
+ if (!noComment && this.ast.comments) {
251
+ this.ast.comments
252
+ .slice()
253
+ .reverse()
254
+ .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
255
+ .forEach((comment) => {
256
+ statements.prepend([
257
+ this.sourceNodeHelper(comment, comment.raw),
258
+ "\n",
259
+ ]);
260
+ });
261
+ }
262
+ const finalExpression = this.formatExpression(last.arguments[0]);
263
+ return { statements, finalExpression };
264
+ }
231
265
  sourceNodeHelper(node, chuncks, name) {
232
266
  const line = node?.loc?.start.line;
233
267
  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);
268
+ // this.fileNameは常にこのMinifyFileインスタンスが担当するモジュール自身の
269
+ // ファイル名(Linkパスで解決済み)なので、ここで出力するノードの由来ファイルとして正しい。
270
+ return new source_map_1.SourceNode(line == undefined ? null : line, column == undefined ? null : column, this.fileName, chuncks, name);
236
271
  }
237
272
  formatStatementList(body) {
238
273
  const result = this.sourceNodeHelper(undefined, []);
@@ -241,7 +276,66 @@ class MinifyFile {
241
276
  });
242
277
  return result;
243
278
  }
279
+ /**
280
+ * SLモード限定: `local x = require("m")` / `x = require("m")` の形(変数1個・
281
+ * 初期化式1個)を、requireを式(IIFE)として埋め込むのではなく、モジュール本体の
282
+ * 文をそのまま展開し最後にターゲットへ代入する「文」の並びとして出力する
283
+ * (#29のレビュー対応。無オプション実行時の挙動互換性のため)。
284
+ *
285
+ * モジュール本体が「単一の式を返すreturn文」で終わっていない場合や、変数・
286
+ * 初期化式が複数ある場合、-mモードの場合は対象外とし、undefinedを返す
287
+ * (呼び出し側は従来のIIFE方式にフォールバックする)。
288
+ */
289
+ trySpliceRequireStatement(statement) {
290
+ if (this.mode.moduleLikeLua) {
291
+ return undefined;
292
+ }
293
+ if (statement.variables.length !== 1 || statement.init.length !== 1) {
294
+ return undefined;
295
+ }
296
+ const moduleRef = this.matchModuleCallExpression(statement.init[0]);
297
+ if (!moduleRef || moduleRef.kind !== "require") {
298
+ return undefined;
299
+ }
300
+ const spliced = this.minifier.splitModuleForStatementSplice(moduleRef.moduleName);
301
+ if (!spliced) {
302
+ return undefined;
303
+ }
304
+ const isLocal = statement.type === "LocalStatement";
305
+ const target = statement.variables[0];
306
+ const targetNode = isLocal
307
+ ? this.generateIdentifier(target)
308
+ : this.formatExpression(target);
309
+ const result = this.sourceNodeHelper(statement, []);
310
+ addWithSeparator(result, spliced.statements);
311
+ addWithSeparator(result, isLocal ? ["local ", targetNode] : [targetNode]);
312
+ addWithSeparator(result, "=");
313
+ addWithSeparator(result, spliced.finalExpression);
314
+ return result;
315
+ }
316
+ /**
317
+ * SLモード限定: `require("m")` を戻り値を使わない単独の文として書いた場合、
318
+ * dofileと同じくfunctionで包まずそのまま展開する(Cプリプロセッサのincludeに
319
+ * 近い、LifeBoat Modeでの一般的な使い方に合わせるための対応。#29のレビュー対応)。
320
+ */
321
+ tryInlineBareRequireStatement(expr) {
322
+ if (this.mode.moduleLikeLua) {
323
+ return undefined;
324
+ }
325
+ const moduleRef = this.matchModuleCallExpression(expr);
326
+ if (!moduleRef || moduleRef.kind !== "require") {
327
+ return undefined;
328
+ }
329
+ return this.minifier.printModuleInline(moduleRef.moduleName);
330
+ }
244
331
  formatStatement(statement) {
332
+ if (statement.type == "AssignmentStatement" ||
333
+ statement.type == "LocalStatement") {
334
+ const spliced = this.trySpliceRequireStatement(statement);
335
+ if (spliced) {
336
+ return spliced;
337
+ }
338
+ }
245
339
  if (statement.type == "AssignmentStatement") {
246
340
  // left-hand side
247
341
  const variables = statement.variables
@@ -273,6 +367,10 @@ class MinifyFile {
273
367
  return result;
274
368
  }
275
369
  else if (statement.type == "CallStatement") {
370
+ const bareRequire = this.tryInlineBareRequireStatement(statement.expression);
371
+ if (bareRequire) {
372
+ return bareRequire;
373
+ }
276
374
  return this.formatExpression(statement.expression); // NOTE: もう一度囲んでもいい
277
375
  }
278
376
  else if (statement.type == "IfStatement") {
@@ -414,13 +512,7 @@ class MinifyFile {
414
512
  }*/
415
513
  formatExpression(expression, argOptions) {
416
514
  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);
515
+ return this.generateIdentifier(expression);
424
516
  }
425
517
  else if (expression.type == "StringLiteral" ||
426
518
  expression.type == "NumericLiteral" ||
@@ -500,22 +592,11 @@ class MinifyFile {
500
592
  return result;
501
593
  }
502
594
  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, ""));
595
+ const moduleRef = this.matchModuleCallExpression(expression);
596
+ if (moduleRef) {
597
+ const replacement = this.formatModuleReference(expression, moduleRef);
598
+ if (replacement) {
599
+ return replacement;
519
600
  }
520
601
  }
521
602
  const args = expression.arguments
@@ -535,6 +616,13 @@ class MinifyFile {
535
616
  ]);
536
617
  }
537
618
  else if (expression.type == "StringCallExpression") {
619
+ const moduleRef = this.matchModuleCallExpression(expression);
620
+ if (moduleRef) {
621
+ const replacement = this.formatModuleReference(expression, moduleRef);
622
+ if (replacement) {
623
+ return replacement;
624
+ }
625
+ }
538
626
  return this.sourceNodeHelper(expression, [
539
627
  this.formatExpression(expression.base),
540
628
  this.formatExpression(expression.argument),
@@ -637,43 +725,58 @@ class MinifyFile {
637
725
  }
638
726
  return result;
639
727
  }
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;
728
+ // require/dofileへの静的な呼び出しを検出する。実際のモジュール解決はLinkパス
729
+ // (Minifier.link)が事前に済ませているため、ここでは呼び出し形が
730
+ // require/dofile への静的文字列呼び出しかどうかを判定するだけでよい(#18)。
731
+ matchModuleCall(base, argument) {
732
+ if (base.type !== "Identifier") {
733
+ return undefined;
734
+ }
735
+ if (base.name !== "require" && base.name !== "dofile") {
736
+ return undefined;
737
+ }
738
+ const moduleName = (0, linker_1.staticStringArgument)(argument);
739
+ if (moduleName === undefined) {
740
+ return undefined;
741
+ }
742
+ return { kind: base.name, moduleName };
743
+ }
744
+ // CallExpression / StringCallExpression のどちらの構文でも呼べるmatchModuleCall
745
+ matchModuleCallExpression(expr) {
746
+ if (expr.type === "CallExpression") {
747
+ return this.matchModuleCall(expr.base, expr.arguments[0]);
748
+ }
749
+ if (expr.type === "StringCallExpression") {
750
+ return this.matchModuleCall(expr.base, expr.argument);
669
751
  }
670
- this.currentIdentifier = "a" + generateZeroes(length);
671
- if (this.minifier.identifiersInUse.has(this.currentIdentifier)) {
672
- return this.generateIdentifier(nameItem, nested);
752
+ return undefined;
753
+ }
754
+ formatModuleReference(expression, ref) {
755
+ if (ref.kind === "dofile") {
756
+ // dofileは呼び出しごとに毎回展開しなおす(キャッシュしない)
757
+ return this.minifier.printModuleInline(ref.moduleName);
758
+ }
759
+ if (!this.mode.moduleLikeLua) {
760
+ // SLモード(無オプション): requireもキャッシュせずその場展開する
761
+ // (挙動互換性のため、ホイストした共有ローカルへの参照にはしない)。
762
+ // 式の位置に置けるようにIIFEで包む。
763
+ const body = this.minifier.printModuleInline(ref.moduleName);
764
+ return this.sourceNodeHelper(expression, [
765
+ "(function() ",
766
+ body,
767
+ " end)()",
768
+ ]);
673
769
  }
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);
770
+ // -mモードのrequireはそのまま呼び出しとして出力し、実行時のrequire関数に委ねる
771
+ return undefined;
772
+ }
773
+ // Renameパス(#20)が解決済みシンボルテーブルをもとに割り当てた短縮名を参照する。
774
+ // 対応するローカルシンボルが無い場合(グローバル参照や"self")は元の名前のまま出力する。
775
+ generateIdentifier(nameItem) {
776
+ const renamed = this.minifier
777
+ .getRenameResult(this.moduleName)
778
+ .nameOf(nameItem);
779
+ return this.sourceNodeHelper(nameItem, renamed ?? nameItem.name, nameItem.name);
677
780
  }
678
781
  }
679
782
  exports.MinifyFile = MinifyFile;
package/dist/cli.js CHANGED
@@ -8,11 +8,15 @@ 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.2")
14
+ .version("0.1.3")
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("--single-line-source-mapping-url", "sourceMappingURLアノテーションを単一行の--コメントで出力します(Source Map仕様の「最終行」ルールに従いますが、既定の複数行ブロックコメント形式を前提とするツールとは組み合わせられません)")
19
+ .option("--strict-source-mapping-url", "sourceMappingURLアノテーションをLuaコメントで一切包まず、Source Map仕様のマーカー文字列(//# sourceMappingURL=...)そのままを出力します。Luaの文法上この形式と有効なLuaコードは両立できないため、出力ファイルの最終行は有効なLua文ではなくなります");
16
20
  program.parse(process.argv);
17
21
  const luaFiles = program.args;
18
22
  const luaparseSetting = {
@@ -21,7 +25,14 @@ const luaparseSetting = {
21
25
  ranges: true,
22
26
  scope: true,
23
27
  };
24
- const mode = program.opts();
28
+ const { singleLineSourceMappingUrl, strictSourceMappingUrl, ...mode } = program.opts();
29
+ // 既定は旧バージョンと互換の複数行ブロックコメント("legacy")。
30
+ // --strict-source-mapping-url > --single-line-source-mapping-url の優先順で上書きする。
31
+ const sourceMappingUrlStyle = strictSourceMappingUrl
32
+ ? "strict"
33
+ : singleLineSourceMappingUrl
34
+ ? "line"
35
+ : "legacy";
25
36
  luaFiles.forEach((fileName) => {
26
37
  const parsedFileName = path_1.default.parse(fileName);
27
38
  if (fs_1.default.existsSync(fileName)) {
@@ -36,10 +47,9 @@ luaFiles.forEach((fileName) => {
36
47
  name: parsedFileName.name,
37
48
  ext: parsedFileName.ext + ".map",
38
49
  });
39
- map.add("\n--[[\n//# sourceMappingURL=" + 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));
50
+ const { code, map: mapJson } = (0, output_1.buildMinifiedOutput)(map, minFileName, mapFileName, { sourceMappingUrlStyle });
51
+ fs_1.default.writeFileSync(minFileName, code);
52
+ fs_1.default.writeFileSync(mapFileName, mapJson);
43
53
  }
44
54
  else {
45
55
  console.error("No such file: " + fileName);
package/dist/index.js CHANGED
@@ -4,38 +4,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
- const path_1 = __importDefault(require("path"));
8
7
  const luaparse_1 = __importDefault(require("luaparse"));
9
8
  const node_process_1 = require("node:process");
10
9
  const ast2lua_1 = require("./ast2lua");
11
10
  const luaparseSetting = {
12
11
  locations: true,
13
- luaVersion: "5.3",
12
+ luaVersion: '5.3',
14
13
  ranges: true,
15
14
  scope: true,
16
15
  };
17
16
  const argPath = node_process_1.argv[2];
18
- const includes = new Set();
19
- function requireHelper(fileName) {
20
- const resolvePath = path_1.default.join(path_1.default.dirname(argPath), fileName.replaceAll(".", path_1.default.sep) + ".lua");
21
- if (!includes.has(resolvePath) && fs_1.default.existsSync(resolvePath)) {
22
- const code = fs_1.default.readFileSync(resolvePath).toString();
23
- const ast = luaparse_1.default.parse(code, luaparseSetting);
24
- if ("globals" in ast) {
25
- return new ast2lua_1.Minifier(resolvePath, ast, requireHelper).parse();
26
- }
27
- }
28
- else {
29
- return undefined;
30
- }
31
- includes.add(resolvePath);
32
- }
33
17
  if (fs_1.default.existsSync(argPath)) {
34
18
  const code = fs_1.default.readFileSync(argPath).toString();
35
19
  const ast = luaparse_1.default.parse(code, luaparseSetting);
36
20
  if ("globals" in ast) {
37
- const map = new ast2lua_1.Minifier(argPath, ast, requireHelper).parse();
38
- map.add("\n--[[\n//# sourceMappingURL=" + argPath + ".map\n]]");
21
+ const map = (0, ast2lua_1.minify)(ast);
22
+ map.add("\n--[[\n//# sourceMappingURL=test.lua.map\n]]");
39
23
  console.log(map.toStringWithSourceMap().code);
40
24
  // toStringWithSourceMap().map の file に書き出したのちのファイル名を入れないとVSCode Extでは検索失敗する
41
25
  //console.log(JSON.stringify(map.toStringWithSourceMap().map));