storm-lua-minify 0.2.0 → 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.
Files changed (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +79 -37
  3. package/dist/ast2lua.js +67 -14
  4. package/dist/cli.js +28 -2
  5. package/dist/globalRename.js +35 -0
  6. package/dist/keywordLocator.js +68 -0
  7. package/dist/linker.js +13 -0
  8. package/dist/minifier.js +75 -1
  9. package/dist/renamer.js +16 -2
  10. package/dist/resolver.js +10 -4
  11. package/dist/transform.js +401 -0
  12. package/package.json +41 -33
  13. package/.github/workflows/ci.yml +0 -39
  14. package/.github/workflows/publish.yml +0 -39
  15. package/.prettierignore +0 -3
  16. package/.prettierrc.json +0 -1
  17. package/eslint.config.js +0 -29
  18. package/src/ast2lua.ts +0 -940
  19. package/src/cli.ts +0 -86
  20. package/src/linker.ts +0 -108
  21. package/src/minifier.ts +0 -284
  22. package/src/output.ts +0 -71
  23. package/src/renamer.ts +0 -134
  24. package/src/resolver.ts +0 -378
  25. package/test/circular-require.test.ts +0 -19
  26. package/test/fixtures/bare-require/main.lua +0 -2
  27. package/test/fixtures/bare-require/mod.lua +0 -1
  28. package/test/fixtures/bitwise-precedence/main.lua +0 -10
  29. package/test/fixtures/circular-require/a.lua +0 -2
  30. package/test/fixtures/circular-require/b.lua +0 -2
  31. package/test/fixtures/circular-require/main.lua +0 -2
  32. package/test/fixtures/dofile/greet.lua +0 -1
  33. package/test/fixtures/dofile/main.lua +0 -2
  34. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +0 -2
  35. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +0 -2
  36. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +0 -2
  37. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +0 -2
  38. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +0 -2
  39. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +0 -2
  40. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +0 -2
  41. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +0 -2
  42. package/test/fixtures/entry-scope-many-requires/dep_india.lua +0 -2
  43. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +0 -2
  44. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +0 -2
  45. package/test/fixtures/entry-scope-many-requires/main.lua +0 -25
  46. package/test/fixtures/multi-require/common.lua +0 -1
  47. package/test/fixtures/multi-require/main.lua +0 -3
  48. package/test/fixtures/nested-module/main.lua +0 -2
  49. package/test/fixtures/nested-module/sub/deep.lua +0 -1
  50. package/test/fixtures/require-call/main.lua +0 -2
  51. package/test/fixtures/require-call/mod.lua +0 -5
  52. package/test/fixtures/require-in-expression/main.lua +0 -1
  53. package/test/fixtures/require-in-expression/mod.lua +0 -1
  54. package/test/fixtures/require-string-call/main.lua +0 -2
  55. package/test/fixtures/require-string-call/mod.lua +0 -5
  56. package/test/fixtures/single-file/main.lua +0 -15
  57. package/test/identifier-collision.test.ts +0 -28
  58. package/test/lib/collision.ts +0 -131
  59. package/test/lib/helpers.ts +0 -124
  60. package/test/no-rename.test.ts +0 -19
  61. package/test/output.test.ts +0 -95
  62. package/test/precedence.test.ts +0 -28
  63. package/test/renamer.test.ts +0 -130
  64. package/test/resolver.test.ts +0 -206
  65. package/test/roundtrip.test.ts +0 -26
  66. package/test/snapshot.test.ts +0 -27
  67. package/test/snapshots/bare-require.sl.lua +0 -1
  68. package/test/snapshots/bitwise-precedence.sl.lua +0 -2
  69. package/test/snapshots/dofile.sl.lua +0 -1
  70. package/test/snapshots/entry-scope-many-requires.m.lua +0 -14
  71. package/test/snapshots/multi-require.m.lua +0 -4
  72. package/test/snapshots/multi-require.sl.lua +0 -1
  73. package/test/snapshots/nested-module.m.lua +0 -4
  74. package/test/snapshots/require-call.m.lua +0 -5
  75. package/test/snapshots/require-call.sl.lua +0 -1
  76. package/test/snapshots/require-in-expression.sl.lua +0 -1
  77. package/test/snapshots/require-string-call.m.lua +0 -5
  78. package/test/snapshots/single-file.sl.lua +0 -6
  79. package/test/sourcemap.test.ts +0 -105
  80. package/tsconfig.eslint.json +0 -8
  81. package/tsconfig.json +0 -111
package/src/cli.ts DELETED
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import { Command } from "commander";
6
- import { Options } from "luaparse";
7
- import { Minifier, MinifierMode } from "./minifier";
8
- import { buildMinifiedOutput, SourceMappingUrlStyle } from "./output";
9
-
10
- const program = new Command();
11
-
12
- program
13
- .version("0.1.3")
14
- .description("A Lua minifier also outputs source map")
15
- .option(
16
- "-m, --module-like-lua",
17
- "require・dofileの動作を実際のLuaに近づけます",
18
- )
19
- .option("--no-rename", "識別子の短縮(リネーム)を無効にします(デバッグ用途)")
20
- .option(
21
- "--single-line-source-mapping-url",
22
- "sourceMappingURLアノテーションを単一行の--コメントで出力します(Source Map仕様の「最終行」ルールに従いますが、既定の複数行ブロックコメント形式を前提とするツールとは組み合わせられません)",
23
- )
24
- .option(
25
- "--strict-source-mapping-url",
26
- "sourceMappingURLアノテーションをLuaコメントで一切包まず、Source Map仕様のマーカー文字列(//# sourceMappingURL=...)そのままを出力します。Luaの文法上この形式と有効なLuaコードは両立できないため、出力ファイルの最終行は有効なLua文ではなくなります",
27
- );
28
-
29
- program.parse(process.argv);
30
-
31
- const luaFiles = program.args;
32
-
33
- const luaparseSetting: Partial<Options> = {
34
- locations: true,
35
- luaVersion: "5.3",
36
- ranges: true,
37
- scope: true,
38
- };
39
-
40
- interface CliOptions extends MinifierMode {
41
- singleLineSourceMappingUrl?: boolean;
42
- strictSourceMappingUrl?: boolean;
43
- }
44
-
45
- const {
46
- singleLineSourceMappingUrl,
47
- strictSourceMappingUrl,
48
- ...mode
49
- }: CliOptions = program.opts();
50
-
51
- // 既定は旧バージョンと互換の複数行ブロックコメント("legacy")。
52
- // --strict-source-mapping-url > --single-line-source-mapping-url の優先順で上書きする。
53
- const sourceMappingUrlStyle: SourceMappingUrlStyle = strictSourceMappingUrl
54
- ? "strict"
55
- : singleLineSourceMappingUrl
56
- ? "line"
57
- : "legacy";
58
-
59
- luaFiles.forEach((fileName) => {
60
- const parsedFileName = path.parse(fileName);
61
-
62
- if (fs.existsSync(fileName)) {
63
- const map = new Minifier(fileName, luaparseSetting, mode).parse();
64
- const minFileName = path.format({
65
- dir: parsedFileName.dir,
66
- name: parsedFileName.name + ".min",
67
- ext: ".lua",
68
- });
69
- const mapFileName = path.format({
70
- dir: parsedFileName.dir,
71
- name: parsedFileName.name,
72
- ext: parsedFileName.ext + ".map",
73
- });
74
- const { code, map: mapJson } = buildMinifiedOutput(
75
- map,
76
- minFileName,
77
- mapFileName,
78
- { sourceMappingUrlStyle },
79
- );
80
-
81
- fs.writeFileSync(minFileName, code);
82
- fs.writeFileSync(mapFileName, mapJson);
83
- } else {
84
- console.error("No such file: " + fileName);
85
- }
86
- });
package/src/linker.ts DELETED
@@ -1,108 +0,0 @@
1
- import { Chunk } from "./ast2lua";
2
-
3
- export interface ModuleReference {
4
- kind: "require" | "dofile";
5
- moduleName: string;
6
- }
7
-
8
- /**
9
- * Chunk配下を型を問わず再帰的に走査するジェネリックウォーカー。
10
- * printerとは独立に、AST全体からrequire/dofile呼び出しを見つけ出すために使う(#18)。
11
- */
12
- function walk(node: unknown, visit: (n: Record<string, unknown>) => void) {
13
- if (node === null || typeof node !== "object") {
14
- return;
15
- }
16
- if (Array.isArray(node)) {
17
- node.forEach((child) => {
18
- walk(child, visit);
19
- });
20
- return;
21
- }
22
- const obj = node as Record<string, unknown>;
23
- if (typeof obj.type === "string") {
24
- visit(obj);
25
- }
26
- for (const key of Object.keys(obj)) {
27
- if (key === "loc" || key === "range") {
28
- continue;
29
- }
30
- walk(obj[key], visit);
31
- }
32
- }
33
-
34
- // luaparseはデフォルト設定(encodingMode: "none")ではStringLiteral.valueを
35
- // 常にnullにする(discardStrings)ため、rawから引用符を取り除いて文字列値を得る。
36
- // require/dofileのモジュール名として使う簡単な文字列リテラルのみを想定しており、
37
- // エスケープシーケンスの解釈までは行わない。
38
- function unquoteRaw(raw: string): string {
39
- if (raw.length >= 2) {
40
- const first = raw.charAt(0);
41
- const last = raw.charAt(raw.length - 1);
42
- if ((first === '"' || first === "'") && first === last) {
43
- return raw.slice(1, -1);
44
- }
45
- }
46
- return raw;
47
- }
48
-
49
- export function staticStringArgument(node: unknown): string | undefined {
50
- if (
51
- node !== null &&
52
- typeof node === "object" &&
53
- (node as Record<string, unknown>).type === "StringLiteral" &&
54
- typeof (node as Record<string, unknown>).raw === "string"
55
- ) {
56
- return unquoteRaw((node as Record<string, unknown>).raw as string);
57
- }
58
- return undefined;
59
- }
60
-
61
- function calleeName(node: unknown): string | undefined {
62
- if (
63
- node !== null &&
64
- typeof node === "object" &&
65
- (node as Record<string, unknown>).type === "Identifier" &&
66
- typeof (node as Record<string, unknown>).name === "string"
67
- ) {
68
- return (node as Record<string, unknown>).name as string;
69
- }
70
- return undefined;
71
- }
72
-
73
- /**
74
- * ASTを走査してrequire/dofile呼び出し(CallExpression / StringCallExpression の両構文)を
75
- * 静的な文字列引数付きのものに限って列挙する。同一モジュールへの参照は重複したまま返す
76
- * (呼び出し側で重複排除する)。
77
- */
78
- export function findModuleReferences(ast: Chunk): ModuleReference[] {
79
- const refs: ModuleReference[] = [];
80
-
81
- walk(ast, (node) => {
82
- if (node.type === "CallExpression") {
83
- const name = calleeName(node.base);
84
- if (name !== "require" && name !== "dofile") {
85
- return;
86
- }
87
- const args = node.arguments;
88
- if (!Array.isArray(args) || args.length === 0) {
89
- return;
90
- }
91
- const moduleName = staticStringArgument(args[0]);
92
- if (moduleName !== undefined) {
93
- refs.push({ kind: name, moduleName });
94
- }
95
- } else if (node.type === "StringCallExpression") {
96
- const name = calleeName(node.base);
97
- if (name !== "require" && name !== "dofile") {
98
- return;
99
- }
100
- const moduleName = staticStringArgument(node.argument);
101
- if (moduleName !== undefined) {
102
- refs.push({ kind: name, moduleName });
103
- }
104
- }
105
- });
106
-
107
- return refs;
108
- }
package/src/minifier.ts DELETED
@@ -1,284 +0,0 @@
1
- import Parser, { Options } from "luaparse";
2
- import path from "path";
3
- import fs from "fs";
4
- import { SourceNode } from "source-map";
5
- import { Chunk, MinifyFile } from "./ast2lua";
6
- import { findModuleReferences } from "./linker";
7
- import { resolveScopes, ResolveResult } from "./resolver";
8
- import { assignRenames, RenameResult } from "./renamer";
9
-
10
- export interface MinifierMode {
11
- moduleLikeLua: boolean;
12
- // 識別子の短縮(リネーム)を行うかどうか。デバッグ用途でfalseにできる。省略時はtrue扱い。
13
- rename?: boolean;
14
- }
15
-
16
- const NO_RENAME: RenameResult = {
17
- nameOf: () => undefined,
18
- usedNames: new Set(),
19
- };
20
-
21
- export class Minifier {
22
- readonly identifiersInUse: Set<string>;
23
- readonly moduleSourceText: Map<string, string>;
24
- readonly moduleAST: Map<string, Chunk>;
25
- readonly moduleNameAndFileName: Map<string, string>;
26
- readonly dir: string;
27
- readonly entryModule: string;
28
- readonly mode: MinifierMode;
29
- readonly luaParseSettings: Partial<Options>;
30
-
31
- // Linkパスで解決されたモジュール名を、依存されている側が先に来る順序で並べたもの
32
- private readonly linkOrder: string[] = [];
33
- // モジュールごとのResolveパスの結果(Linkパスで一度だけ計算し使い回す)
34
- private readonly moduleResolve = new Map<string, ResolveResult>();
35
- // モジュールごとのRenameパスの結果(初回アクセス時に計算しキャッシュする)
36
- private readonly renameCache = new Map<string, RenameResult>();
37
-
38
- constructor(
39
- entryFilePath: string,
40
- luaParseSettings: Partial<Options>,
41
- mode: MinifierMode,
42
- ) {
43
- this.identifiersInUse = new Set<string>();
44
- this.moduleSourceText = new Map<string, string>();
45
- this.moduleAST = new Map<string, Chunk>();
46
- this.moduleNameAndFileName = new Map<string, string>();
47
- this.luaParseSettings = luaParseSettings;
48
- this.mode = mode;
49
- const pn = path.parse(entryFilePath);
50
- this.dir = pn.dir;
51
- this.entryModule = pn.name;
52
- }
53
-
54
- parse(): SourceNode {
55
- this.link();
56
- this.renameAll();
57
-
58
- const parts: (SourceNode | string)[] = [];
59
-
60
- const entryComments = this.moduleAST.get(this.entryModule)?.comments;
61
- if (entryComments) {
62
- entryComments
63
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
64
- .forEach((comment) => {
65
- parts.push(
66
- new SourceNode(
67
- comment.loc?.start.line ?? null,
68
- comment.loc?.start.column ?? null,
69
- this.moduleNameAndFileName.get(this.entryModule) ?? null,
70
- comment.raw,
71
- ),
72
- "\n",
73
- );
74
- });
75
- }
76
-
77
- if (this.mode.moduleLikeLua) {
78
- parts.push(this.buildRequireWrapper());
79
- }
80
-
81
- parts.push(this.printModule(this.entryModule));
82
-
83
- const result = new SourceNode(null, null, null, parts);
84
-
85
- this.moduleSourceText.forEach((v, k) => {
86
- const fileName = this.moduleNameAndFileName.get(k);
87
- if (fileName) {
88
- result.setSourceContent(fileName, v);
89
- }
90
- });
91
-
92
- return result;
93
- }
94
-
95
- /**
96
- * dofileの呼び出し箇所ごとに、キャッシュ済みASTから新規にSourceNodeを作り直す。
97
- * 同じSourceNodeインスタンスを複数箇所へ挿入すると壊れるため、常に作り直す(#18)。
98
- */
99
- printModuleInline(moduleName: string): SourceNode {
100
- return this.printModule(moduleName);
101
- }
102
-
103
- /**
104
- * requireを式(IIFE)ではなく文として展開できる場合に使う。モジュール本体が
105
- * 「単一の式を返すreturn文」で終わっている場合のみ結果を返す。それ以外は
106
- * undefinedを返すので、呼び出し側は従来のIIFE方式にフォールバックする(#29)。
107
- */
108
- splitModuleForStatementSplice(
109
- moduleName: string,
110
- ): { statements: SourceNode; finalExpression: SourceNode } | undefined {
111
- const ast = this.moduleAST.get(moduleName);
112
- const fileName = this.moduleNameAndFileName.get(moduleName);
113
- if (!ast || !fileName) {
114
- throw new Error(moduleName + " is not found");
115
- }
116
- return new MinifyFile(
117
- fileName,
118
- moduleName,
119
- ast,
120
- this,
121
- this.mode,
122
- ).parseAsStatementsAndFinalExpression(moduleName === this.entryModule);
123
- }
124
-
125
- /**
126
- * 指定モジュールのRenameパス結果を返す。`renameAll`で事前に計算済みの
127
- * ものをそのまま返すだけの参照用アクセサ。
128
- */
129
- getRenameResult(moduleName: string): RenameResult {
130
- if (this.mode.rename === false) {
131
- return NO_RENAME;
132
- }
133
- const cached = this.renameCache.get(moduleName);
134
- if (!cached) {
135
- throw new Error(moduleName + " is not found");
136
- }
137
- return cached;
138
- }
139
-
140
- /**
141
- * Renameパス(#20): linkOrder(依存されている側が先)の順にモジュールごとの
142
- * 短縮名を割り当てる。
143
- *
144
- * dofileやSLモードのrequireその場展開は、呼び出し元と同じLuaスコープに
145
- * 関数で包まずに直接展開されるため、モジュールをまたいで同じ短縮名を
146
- * 再利用すると本来無関係な変数同士が衝突しうる(#12)。これを安全に防ぐため、
147
- * あるモジュールが実際に使った短縮名は、後続モジュールを処理する前に
148
- * `identifiersInUse`(予約名の集合)へ積み増す。これにより短縮名は
149
- * プログラム全体で重複しなくなる(モジュール間での再利用による圧縮は
150
- * 犠牲になるが、モジュール内でのスコープに基づく再利用は維持される)。
151
- */
152
- private renameAll() {
153
- if (this.mode.rename === false) {
154
- return;
155
- }
156
- this.linkOrder.forEach((moduleName) => {
157
- const resolved = this.moduleResolve.get(moduleName);
158
- if (!resolved) {
159
- throw new Error(moduleName + " is not found");
160
- }
161
- const result = assignRenames(resolved, this.identifiersInUse);
162
- this.renameCache.set(moduleName, result);
163
- result.usedNames.forEach((name) => this.identifiersInUse.add(name));
164
- });
165
- }
166
-
167
- private printModule(moduleName: string): SourceNode {
168
- const ast = this.moduleAST.get(moduleName);
169
- const fileName = this.moduleNameAndFileName.get(moduleName);
170
- if (!ast || !fileName) {
171
- throw new Error(moduleName + " is not found");
172
- }
173
- return new MinifyFile(fileName, moduleName, ast, this, this.mode).parse(
174
- moduleName === this.entryModule,
175
- );
176
- }
177
-
178
- /**
179
- * エントリファイルから到達可能な全モジュールをASTレベルで解決するLinkパス(#18)。
180
- * - ファイルごとのパースは一度だけ行う(同一モジュールの多重require/dofileの重複排除)
181
- * - require/dofileの参照グラフに循環があればエラーを投げる
182
- * - 出力(Print)を開始する前に、必要なモジュール解決をすべて完了させる
183
- */
184
- private link() {
185
- const visiting = new Set<string>();
186
- const stack: string[] = [];
187
-
188
- const visit = (moduleName: string) => {
189
- if (visiting.has(moduleName)) {
190
- const cycleStart = stack.indexOf(moduleName);
191
- const cycle = [...stack.slice(cycleStart), moduleName];
192
- throw new Error(
193
- "Circular require/dofile detected: " + cycle.join(" -> "),
194
- );
195
- }
196
- if (this.moduleAST.has(moduleName)) {
197
- // 解決済み(このモジュールは複数箇所から参照されていても一度しかパースしない)
198
- return;
199
- }
200
-
201
- visiting.add(moduleName);
202
- stack.push(moduleName);
203
-
204
- const fullResolvePath =
205
- path.join(this.dir, ...moduleName.split(".")) + ".lua";
206
- if (!fs.existsSync(fullResolvePath)) {
207
- throw new Error(moduleName + " is not found");
208
- }
209
- const code = fs.readFileSync(fullResolvePath).toString();
210
- const ast = Parser.parse(code, this.luaParseSettings) as Chunk;
211
-
212
- // Resolveパス(#19): このモジュールのスコープ/シンボルを解析し、Renameパスの
213
- // 入力として使い回せるようキャッシュする。グローバル参照はプログラム全体で
214
- // 予約すべき名前(identifiersInUse)としてここで集計する。
215
- const resolved = resolveScopes(ast);
216
- this.moduleResolve.set(moduleName, resolved);
217
- resolved.globals.forEach((binding) =>
218
- this.identifiersInUse.add(binding.name),
219
- );
220
-
221
- this.moduleSourceText.set(moduleName, code);
222
- this.moduleAST.set(moduleName, ast);
223
- // Source Mapの`sources`はURLとして解釈されるため、OS依存のpath.sepではなく
224
- // 常に"/"区切りで保持する(Windows上でのビルドでも壊れないように)。
225
- this.moduleNameAndFileName.set(
226
- moduleName,
227
- moduleName.replaceAll(".", "/") + ".lua",
228
- );
229
-
230
- findModuleReferences(ast).forEach((ref) => {
231
- visit(ref.moduleName);
232
- });
233
-
234
- visiting.delete(moduleName);
235
- stack.pop();
236
- this.linkOrder.push(moduleName);
237
- };
238
-
239
- visit(this.entryModule);
240
- }
241
-
242
- /**
243
- * require()(dofileは除く)で参照されているモジュール名の集合を求める。
244
- * dofileは呼び出しごとに毎回展開しなおすため、キャッシュ/ホイストの対象にしない。
245
- */
246
- private collectRequireTargets(): Set<string> {
247
- const targets = new Set<string>();
248
- this.linkOrder.forEach((moduleName) => {
249
- const ast = this.moduleAST.get(moduleName);
250
- if (!ast) {
251
- return;
252
- }
253
- findModuleReferences(ast).forEach((ref) => {
254
- if (ref.kind === "require") {
255
- targets.add(ref.moduleName);
256
- }
257
- });
258
- });
259
- return targets;
260
- }
261
-
262
- private buildRequireWrapper(): SourceNode {
263
- const targets = this.collectRequireTargets();
264
- const parts: (SourceNode | string)[] = [
265
- "function require(m,r)package=package or{loaded={}};if package.loaded[m]then return package.loaded[m]end\n",
266
- ];
267
- this.linkOrder.forEach((moduleName) => {
268
- if (moduleName === this.entryModule || !targets.has(moduleName)) {
269
- return;
270
- }
271
- parts.push(
272
- 'if m=="',
273
- moduleName,
274
- '"then r=(function() ',
275
- this.printModule(moduleName),
276
- " end)()end\n",
277
- );
278
- });
279
- parts.push(
280
- "package.loaded[m]=package.loaded[m]or r or true;return package.loaded[m]end\n",
281
- );
282
- return new SourceNode(null, null, null, parts);
283
- }
284
- }
package/src/output.ts DELETED
@@ -1,71 +0,0 @@
1
- import path from "path";
2
- import { SourceNode } from "source-map";
3
-
4
- export interface MinifiedOutput {
5
- code: string;
6
- map: string;
7
- }
8
-
9
- export type SourceMappingUrlStyle = "legacy" | "line" | "strict";
10
-
11
- export interface BuildMinifiedOutputOptions {
12
- /**
13
- * sourceMappingURLアノテーションの出力形式。省略時は"legacy"。
14
- *
15
- * - "legacy"(既定): 旧storm-lua-minifyと同じ複数行の`--[[ ... ]]`ブロック
16
- * コメント(`--[[\n//# sourceMappingURL=...\n]]`)をそのまま出力する。
17
- * 互換性優先で、この形式を前提に読み込む既存ツールと組み合わせて使う。
18
- * Source Map仕様が定める「アノテーションは生成コードの最後の行(末尾が
19
- * 空行の場合はその直前の行)に置く」というルールには厳密には従わない
20
- * (アノテーション行の後に`]]`が続き最終行にならないため)。
21
- * - "line": 単一行の`--`ラインコメント(`-- //# sourceMappingURL=...`)。
22
- * 末尾の空行を除けば出力の最終行になるため「最終行ルール」は満たすが、
23
- * マーカー文字列自体は`-- `に続く形になる(行頭が厳密に`//`であることを
24
- * 要求する一部のツールからは認識されない可能性がある)。有効なLuaの
25
- * ままである。
26
- * - "strict": Luaコメントで一切包まず`//# sourceMappingURL=...`をそのまま
27
- * 出力する。マーカー文字列・最終行ルールの両方を厳密に満たす。
28
- *
29
- * 【Lua文法との両立について】Lua 5.1〜5.4のどのバージョンでも`/`(除算)や
30
- * `//`(5.3以降の整数除算)は二項演算子のトークンであり、文の先頭に
31
- * 来ることはできない(Luaの文は識別子・キーワード・`::`・`;`のいずれかで
32
- * 始まらなければならない)。そのため`//`から始まる行を、一切のコメント化
33
- * なしにLuaの文として構文解析可能にする方法は存在しない。したがって
34
- * "strict"を選択した場合、出力ファイルの最終行は有効なLua文ではなくなる
35
- * (luaparseで再パースするとエラーになる)。この制約はLuaの言語仕様上
36
- * 回避不能であることを確認済み。
37
- */
38
- sourceMappingUrlStyle?: SourceMappingUrlStyle;
39
- }
40
-
41
- /**
42
- * ミニファイ済みSourceNodeに、sourceMappingURLアノテーションとSource Mapを
43
- * 付加した最終的な出力を組み立てる。アノテーションの出力形式は
44
- * `BuildMinifiedOutputOptions.sourceMappingUrlStyle`で選択する。
45
- */
46
- export function buildMinifiedOutput(
47
- sourceNode: SourceNode,
48
- minFileName: string,
49
- mapFileName: string,
50
- options: BuildMinifiedOutputOptions = {},
51
- ): MinifiedOutput {
52
- const style = options.sourceMappingUrlStyle ?? "legacy";
53
- const marker = "//# sourceMappingURL=" + path.basename(mapFileName);
54
-
55
- if (style === "legacy") {
56
- // 旧storm-lua-minifyと完全に同じ出力(末尾に改行は付加しない)。
57
- sourceNode.add("\n--[[\n" + marker + "\n]]");
58
- } else if (style === "line") {
59
- sourceNode.add("\n-- " + marker + "\n");
60
- } else {
61
- sourceNode.add("\n" + marker + "\n");
62
- }
63
-
64
- const sourceAndMap = sourceNode.toStringWithSourceMap({
65
- file: path.basename(minFileName),
66
- });
67
- return {
68
- code: sourceAndMap.code,
69
- map: JSON.stringify(sourceAndMap.map),
70
- };
71
- }
package/src/renamer.ts DELETED
@@ -1,134 +0,0 @@
1
- // Renameパス(#20): Resolveパス(#19)が構築したシンボルテーブルをもとに、
2
- // シンボル単位で短縮識別子を割り当てる。printer(ast2lua.ts)は出力時の
3
- // その場リネームを行わず、このパスが決めた名前を参照するだけになる。
4
- //
5
- // 割当は2段階で行う。
6
- // 1. スロット割当: スコープ木を辿り、各シンボルに整数のスロットを割り当てる。
7
- // 兄弟スコープ同士(親子関係にないスコープ同士)は生存区間が重ならないため、
8
- // 同じスロットを再利用できる(圧縮率の向上)。同一スコープ内のシンボル同士・
9
- // 祖先スコープのシンボルとは必ず異なるスロットになるため、シンボルの生存
10
- // スコープ内で名前が衝突することはない。
11
- // 2. 名前割当: スロットごとの参照回数(頻度)を集計し、頻度の高いスロットから
12
- // 順に短い名前を割り当てる。頻度の高いシンボルほど短い名前になるため、
13
- // 同じスロット数でも出力サイズが最小化される。
14
- //
15
- // 予約語・"self"・呼び出し側が指定する予約名(グローバル参照やStormworks APIなど)
16
- // は、どのシンボルにも割り当てられない。
17
- import Parser from "luaparse";
18
- import { Scope, Symbol, ResolveResult } from "./resolver";
19
- import { IDENTIFIER_PARTS, isKeyword } from "./ast2lua";
20
-
21
- export interface RenameResult {
22
- // identifierがResolveパスで解決済みのローカルシンボルに対応する場合、
23
- // 割り当てられた短縮名を返す。グローバル参照やフィールド名など対応する
24
- // シンボルが無い場合はundefinedを返す(呼び出し側は元の名前を使う)。
25
- nameOf(identifier: Parser.Identifier): string | undefined;
26
- // このモジュールが実際に割り当てた短縮名の集合。requireの展開先が
27
- // 呼び出し元と同じLuaスコープに直接展開される場合(dofile等、関数で
28
- // 包まれない展開)があるため、他モジュールの割当と衝突しないよう
29
- // 呼び出し側(Minifier)はこれを後続モジュールの予約名に積み増す。
30
- readonly usedNames: ReadonlySet<string>;
31
- }
32
-
33
- function isAvailable(id: string, reserved: ReadonlySet<string>): boolean {
34
- return id !== "self" && !isKeyword(id) && !reserved.has(id);
35
- }
36
-
37
- // 0始まりのカウンタから短縮名候補を生成する(バイジェクティブ基数記数法)。
38
- // 通常の位取り記数法と違い同じ文字列を2つのカウンタ値が指すことがないため、
39
- // カウンタを増やし続けるだけで重複なく識別子候補を列挙できる。
40
- function generateCandidate(counter: number): string {
41
- const l = IDENTIFIER_PARTS.length;
42
- let num = counter + 1;
43
- let id = "";
44
- while (num > 0) {
45
- const rem = (num - 1) % l;
46
- id = IDENTIFIER_PARTS[rem] + id;
47
- num = Math.floor((num - 1) / l);
48
- }
49
- return id;
50
- }
51
-
52
- /**
53
- * スコープ木のDFSでシンボルごとにスロット番号を割り当てる。
54
- * `active`は祖先スコープ(自分を含む)で既に使われているスロットの集合。
55
- * 兄弟スコープには同じ`active`のコピーが渡されるため、互いの割当は影響しない。
56
- */
57
- function assignSlots(
58
- scope: Scope,
59
- active: ReadonlySet<number>,
60
- ): Map<Symbol, number> {
61
- const slotOf = new Map<Symbol, number>();
62
- const used = new Set(active);
63
-
64
- scope.symbols.forEach((symbol) => {
65
- let slot = 0;
66
- while (used.has(slot)) {
67
- slot++;
68
- }
69
- slotOf.set(symbol, slot);
70
- used.add(slot);
71
- });
72
-
73
- scope.children.forEach((child) => {
74
- assignSlots(child, used).forEach((slot, symbol) => {
75
- slotOf.set(symbol, slot);
76
- });
77
- });
78
-
79
- return slotOf;
80
- }
81
-
82
- export function assignRenames(
83
- resolveResult: ResolveResult,
84
- reserved: ReadonlySet<string>,
85
- ): RenameResult {
86
- const slotOf = assignSlots(resolveResult.chunkScope, new Set());
87
-
88
- // スロットの通算参照回数(宣言自体も1回として数える)を集計する。
89
- const weightOfSlot = new Map<number, number>();
90
- resolveResult.symbols.forEach((symbol) => {
91
- const slot = slotOf.get(symbol);
92
- if (slot === undefined) {
93
- return;
94
- }
95
- const weight = symbol.references.length + 1;
96
- weightOfSlot.set(slot, (weightOfSlot.get(slot) ?? 0) + weight);
97
- });
98
-
99
- const orderedSlots = [...weightOfSlot.keys()].sort(
100
- (a, b) => (weightOfSlot.get(b) ?? 0) - (weightOfSlot.get(a) ?? 0),
101
- );
102
-
103
- const nameOfSlot = new Map<number, string>();
104
- let counter = 0;
105
- orderedSlots.forEach((slot) => {
106
- let candidate: string;
107
- do {
108
- candidate = generateCandidate(counter++);
109
- } while (!isAvailable(candidate, reserved));
110
- nameOfSlot.set(slot, candidate);
111
- });
112
-
113
- const nameOfSymbol = new Map<Symbol, string>();
114
- slotOf.forEach((slot, symbol) => {
115
- const name = nameOfSlot.get(slot);
116
- if (name !== undefined) {
117
- nameOfSymbol.set(symbol, name);
118
- }
119
- });
120
-
121
- return {
122
- nameOf: (identifier) => {
123
- // メソッド定義の暗黙のselfパラメータは慣習的な名前のため常に維持する
124
- // (呼び出し側から見える名前ではないため短縮しても安全ではあるが、
125
- // 可読性のために元の名前のままにする)。
126
- if (identifier.name === "self") {
127
- return undefined;
128
- }
129
- const symbol = resolveResult.symbolOf(identifier);
130
- return symbol ? nameOfSymbol.get(symbol) : undefined;
131
- },
132
- usedNames: new Set(nameOfSlot.values()),
133
- };
134
- }