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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyAndRenameGlobals = classifyAndRenameGlobals;
|
|
4
|
+
const renamer_1 = require("./renamer");
|
|
5
|
+
const linker_1 = require("./linker");
|
|
6
|
+
function classifyAndRenameGlobals(moduleResolve, neverRename, reserved) {
|
|
7
|
+
const everWritten = new Set();
|
|
8
|
+
const totalReferenceCount = new Map();
|
|
9
|
+
moduleResolve.forEach((resolved) => {
|
|
10
|
+
resolved.globals.forEach((binding) => {
|
|
11
|
+
if (binding.writes.length > 0) {
|
|
12
|
+
everWritten.add(binding.name);
|
|
13
|
+
}
|
|
14
|
+
totalReferenceCount.set(binding.name, (totalReferenceCount.get(binding.name) ?? 0) +
|
|
15
|
+
binding.references.length);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
// require/dofileはPrintパスが名前文字列で再検出するため、通常は代入対象に
|
|
19
|
+
// ならないが(そのため既にeverWrittenに入らない)、ユーザーコードが
|
|
20
|
+
// `require = ...`のように再代入する非現実的なケースへの防御として明示的にも除外する。
|
|
21
|
+
const qualifying = [...everWritten].filter((name) => !neverRename.has(name) && !linker_1.RESERVED_MODULE_FUNCTION_NAMES.has(name));
|
|
22
|
+
const orderedNames = qualifying.sort((a, b) => (totalReferenceCount.get(b) ?? 0) - (totalReferenceCount.get(a) ?? 0));
|
|
23
|
+
const usedShortNames = new Set(reserved);
|
|
24
|
+
const globalRenames = new Map();
|
|
25
|
+
let counter = 0;
|
|
26
|
+
orderedNames.forEach((name) => {
|
|
27
|
+
let candidate;
|
|
28
|
+
do {
|
|
29
|
+
candidate = (0, renamer_1.generateCandidate)(counter++);
|
|
30
|
+
} while (!(0, renamer_1.isAvailable)(candidate, usedShortNames));
|
|
31
|
+
usedShortNames.add(candidate);
|
|
32
|
+
globalRenames.set(name, candidate);
|
|
33
|
+
});
|
|
34
|
+
return globalRenames;
|
|
35
|
+
}
|
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:
|
|
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 =
|
|
38
|
-
map.add("\n--[[\n//# sourceMappingURL=
|
|
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));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.KeywordLocator = void 0;
|
|
7
|
+
const luaparse_1 = __importDefault(require("luaparse"));
|
|
8
|
+
// luaparseの型定義(@types/luaparse)には`tokenTypes`が含まれないため、
|
|
9
|
+
// 実行時の値を直接参照する。luaparse 0.3.1のソース上の定義は
|
|
10
|
+
// `var EOF = 1, StringLiteral = 2, Keyword = 4, ...`。
|
|
11
|
+
const tokenTypes = luaparse_1.default.tokenTypes;
|
|
12
|
+
function comparePosition(a, b) {
|
|
13
|
+
return a.line !== b.line ? a.line - b.line : a.column - b.column;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* luaparseのASTは文・式の境界にしか`loc`を持たず、`then`/`do`/`until`のような
|
|
17
|
+
* キーワード単体の出現位置は保持しない。このクラスはluaparseの低レベル
|
|
18
|
+
* 再トークン化API(`parse(code, {wait: true, ...})` + `lex()`)でモジュールの
|
|
19
|
+
* ソースを再走査し、キーワードトークンの正確な位置を検索できるようにする(#14)。
|
|
20
|
+
*/
|
|
21
|
+
class KeywordLocator {
|
|
22
|
+
tokens;
|
|
23
|
+
constructor(sourceText, luaParseSettings) {
|
|
24
|
+
const parser = luaparse_1.default.parse(sourceText, {
|
|
25
|
+
...luaParseSettings,
|
|
26
|
+
wait: true,
|
|
27
|
+
});
|
|
28
|
+
const tokens = [];
|
|
29
|
+
for (;;) {
|
|
30
|
+
const token = parser.lex();
|
|
31
|
+
if (token.type === tokenTypes.EOF) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (token.type === tokenTypes.Keyword) {
|
|
35
|
+
tokens.push({
|
|
36
|
+
value: token.value,
|
|
37
|
+
line: token.line,
|
|
38
|
+
column: token.range[0] - token.lineStart,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.tokens = tokens;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `from`(含む)以降で最初に現れる、値が`value`と一致するキーワードトークンの
|
|
46
|
+
* 開始位置を返す。見つからない場合はundefined。
|
|
47
|
+
*/
|
|
48
|
+
findFrom(from, value) {
|
|
49
|
+
let lo = 0;
|
|
50
|
+
let hi = this.tokens.length;
|
|
51
|
+
while (lo < hi) {
|
|
52
|
+
const mid = (lo + hi) >>> 1;
|
|
53
|
+
if (comparePosition(this.tokens[mid], from) < 0) {
|
|
54
|
+
lo = mid + 1;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
hi = mid;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (let i = lo; i < this.tokens.length; i++) {
|
|
61
|
+
if (this.tokens[i].value === value) {
|
|
62
|
+
return { line: this.tokens[i].line, column: this.tokens[i].column };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.KeywordLocator = KeywordLocator;
|
package/dist/linker.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESERVED_MODULE_FUNCTION_NAMES = void 0;
|
|
4
|
+
exports.staticStringArgument = staticStringArgument;
|
|
5
|
+
exports.findModuleReferences = findModuleReferences;
|
|
6
|
+
// require/dofileは、Linkパス(このファイルのfindModuleReferences)だけでなく
|
|
7
|
+
// Printパス(ast2lua.tsのmatchModuleCallExpression、minifier.tsの
|
|
8
|
+
// buildRequireWrapper/collectRequireTargets)でも、この2つの名前文字列を
|
|
9
|
+
// 手がかりに呼び出しを再検出している。そのため、識別子リネーム系のパス
|
|
10
|
+
// (#8a: globalRename.ts、#8b: transform.tsのinsertGlobalAliases)は、
|
|
11
|
+
// これらの名前を書き換え候補から常に除外しなければならない。書き換えると
|
|
12
|
+
// Printパスがrequire/dofile呼び出しを検出できなくなり、モジュール解決が
|
|
13
|
+
// 静かに壊れる(ディスパッチテーブルの生成漏れ等)。
|
|
14
|
+
exports.RESERVED_MODULE_FUNCTION_NAMES = new Set([
|
|
15
|
+
"require",
|
|
16
|
+
"dofile",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Chunk配下を型を問わず再帰的に走査するジェネリックウォーカー。
|
|
20
|
+
* printerとは独立に、AST全体からrequire/dofile呼び出しを見つけ出すために使う(#18)。
|
|
21
|
+
*/
|
|
22
|
+
function walk(node, visit) {
|
|
23
|
+
if (node === null || typeof node !== "object") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(node)) {
|
|
27
|
+
node.forEach((child) => {
|
|
28
|
+
walk(child, visit);
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const obj = node;
|
|
33
|
+
if (typeof obj.type === "string") {
|
|
34
|
+
visit(obj);
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(obj)) {
|
|
37
|
+
if (key === "loc" || key === "range") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
walk(obj[key], visit);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// luaparseはデフォルト設定(encodingMode: "none")ではStringLiteral.valueを
|
|
44
|
+
// 常にnullにする(discardStrings)ため、rawから引用符を取り除いて文字列値を得る。
|
|
45
|
+
// require/dofileのモジュール名として使う簡単な文字列リテラルのみを想定しており、
|
|
46
|
+
// エスケープシーケンスの解釈までは行わない。
|
|
47
|
+
function unquoteRaw(raw) {
|
|
48
|
+
if (raw.length >= 2) {
|
|
49
|
+
const first = raw.charAt(0);
|
|
50
|
+
const last = raw.charAt(raw.length - 1);
|
|
51
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
52
|
+
return raw.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
function staticStringArgument(node) {
|
|
58
|
+
if (node !== null &&
|
|
59
|
+
typeof node === "object" &&
|
|
60
|
+
node.type === "StringLiteral" &&
|
|
61
|
+
typeof node.raw === "string") {
|
|
62
|
+
return unquoteRaw(node.raw);
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function calleeName(node) {
|
|
67
|
+
if (node !== null &&
|
|
68
|
+
typeof node === "object" &&
|
|
69
|
+
node.type === "Identifier" &&
|
|
70
|
+
typeof node.name === "string") {
|
|
71
|
+
return node.name;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* ASTを走査してrequire/dofile呼び出し(CallExpression / StringCallExpression の両構文)を
|
|
77
|
+
* 静的な文字列引数付きのものに限って列挙する。同一モジュールへの参照は重複したまま返す
|
|
78
|
+
* (呼び出し側で重複排除する)。
|
|
79
|
+
*/
|
|
80
|
+
function findModuleReferences(ast) {
|
|
81
|
+
const refs = [];
|
|
82
|
+
walk(ast, (node) => {
|
|
83
|
+
if (node.type === "CallExpression") {
|
|
84
|
+
const name = calleeName(node.base);
|
|
85
|
+
if (name !== "require" && name !== "dofile") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const args = node.arguments;
|
|
89
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const moduleName = staticStringArgument(args[0]);
|
|
93
|
+
if (moduleName !== undefined) {
|
|
94
|
+
refs.push({ kind: name, moduleName });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (node.type === "StringCallExpression") {
|
|
98
|
+
const name = calleeName(node.base);
|
|
99
|
+
if (name !== "require" && name !== "dofile") {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const moduleName = staticStringArgument(node.argument);
|
|
103
|
+
if (moduleName !== undefined) {
|
|
104
|
+
refs.push({ kind: name, moduleName });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return refs;
|
|
109
|
+
}
|
package/dist/minifier.js
CHANGED
|
@@ -9,22 +9,35 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const source_map_1 = require("source-map");
|
|
11
11
|
const ast2lua_1 = require("./ast2lua");
|
|
12
|
+
const linker_1 = require("./linker");
|
|
13
|
+
const resolver_1 = require("./resolver");
|
|
14
|
+
const renamer_1 = require("./renamer");
|
|
15
|
+
const globalRename_1 = require("./globalRename");
|
|
16
|
+
const transform_1 = require("./transform");
|
|
17
|
+
const NO_RENAME = {
|
|
18
|
+
nameOf: () => undefined,
|
|
19
|
+
usedNames: new Set(),
|
|
20
|
+
};
|
|
12
21
|
class Minifier {
|
|
13
|
-
identifierMap;
|
|
14
22
|
identifiersInUse;
|
|
15
23
|
moduleSourceText;
|
|
16
|
-
moduleSourceNode;
|
|
17
24
|
moduleAST;
|
|
18
25
|
moduleNameAndFileName;
|
|
19
26
|
dir;
|
|
20
27
|
entryModule;
|
|
21
28
|
mode;
|
|
22
29
|
luaParseSettings;
|
|
30
|
+
// Linkパスで解決されたモジュール名を、依存されている側が先に来る順序で並べたもの
|
|
31
|
+
linkOrder = [];
|
|
32
|
+
// モジュールごとのResolveパスの結果(Linkパスで一度だけ計算し使い回す)
|
|
33
|
+
moduleResolve = new Map();
|
|
34
|
+
// モジュールごとのRenameパスの結果(初回アクセス時に計算しキャッシュする)
|
|
35
|
+
renameCache = new Map();
|
|
36
|
+
// #8a: プログラム全体を横断して決定された「内部グローバル名 -> 短縮名」の対応
|
|
37
|
+
globalRenames = new Map();
|
|
23
38
|
constructor(entryFilePath, luaParseSettings, mode) {
|
|
24
|
-
this.identifierMap = new Map();
|
|
25
39
|
this.identifiersInUse = new Set();
|
|
26
40
|
this.moduleSourceText = new Map();
|
|
27
|
-
this.moduleSourceNode = new Map();
|
|
28
41
|
this.moduleAST = new Map();
|
|
29
42
|
this.moduleNameAndFileName = new Map();
|
|
30
43
|
this.luaParseSettings = luaParseSettings;
|
|
@@ -34,65 +47,247 @@ class Minifier {
|
|
|
34
47
|
this.entryModule = pn.name;
|
|
35
48
|
}
|
|
36
49
|
parse() {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
package.loaded[m]=package.loaded[m] or r or true;return package.loaded[m]
|
|
46
|
-
end
|
|
47
|
-
*/
|
|
48
|
-
sn.prepend("package.loaded[m]=package.loaded[m]or r or true;return package.loaded[m]end\n");
|
|
49
|
-
this.moduleSourceNode.forEach((v, k) => {
|
|
50
|
-
if (k !== this.entryModule) {
|
|
51
|
-
sn.prepend(["if m==\"", k, "\"then r=(function() ", v, " end)()end\n"]);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
sn.prepend("function require(m,r)package=package or{loaded={}};if package.loaded[m]then return package.loaded[m]end\n");
|
|
55
|
-
}
|
|
56
|
-
// コメントの流し込み
|
|
57
|
-
const comments = this.moduleAST.get(this.entryModule)?.comments;
|
|
58
|
-
if (comments) {
|
|
59
|
-
comments
|
|
60
|
-
.reverse()
|
|
50
|
+
this.link();
|
|
51
|
+
this.computeGlobalRenames();
|
|
52
|
+
this.transformAll();
|
|
53
|
+
this.renameAll();
|
|
54
|
+
const parts = [];
|
|
55
|
+
const entryComments = this.moduleAST.get(this.entryModule)?.comments;
|
|
56
|
+
if (entryComments) {
|
|
57
|
+
entryComments
|
|
61
58
|
.filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
|
|
62
59
|
.forEach((comment) => {
|
|
63
|
-
|
|
64
|
-
new source_map_1.SourceNode(comment.loc?.start.line || null, comment.loc?.start.column || null, this.entryModule, // 本当に自分のファイル名でよいかは要検討
|
|
65
|
-
comment.raw),
|
|
66
|
-
"\n"
|
|
67
|
-
]);
|
|
60
|
+
parts.push(new source_map_1.SourceNode(comment.loc?.start.line ?? null, comment.loc?.start.column ?? null, this.moduleNameAndFileName.get(this.entryModule) ?? null, comment.raw), "\n");
|
|
68
61
|
});
|
|
69
62
|
}
|
|
70
|
-
this.
|
|
71
|
-
|
|
63
|
+
if (this.mode.moduleLikeLua) {
|
|
64
|
+
parts.push(this.buildRequireWrapper());
|
|
65
|
+
}
|
|
66
|
+
parts.push(this.printModule(this.entryModule));
|
|
67
|
+
const result = new source_map_1.SourceNode(null, null, null, parts);
|
|
68
|
+
this.moduleSourceText.forEach((v, k) => {
|
|
69
|
+
const fileName = this.moduleNameAndFileName.get(k);
|
|
70
|
+
if (fileName) {
|
|
71
|
+
result.setSourceContent(fileName, v);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* dofileの呼び出し箇所ごとに、キャッシュ済みASTから新規にSourceNodeを作り直す。
|
|
78
|
+
* 同じSourceNodeインスタンスを複数箇所へ挿入すると壊れるため、常に作り直す(#18)。
|
|
79
|
+
*/
|
|
80
|
+
printModuleInline(moduleName) {
|
|
81
|
+
return this.printModule(moduleName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* requireを式(IIFE)ではなく文として展開できる場合に使う。モジュール本体が
|
|
85
|
+
* 「単一の式を返すreturn文」で終わっている場合のみ結果を返す。それ以外は
|
|
86
|
+
* undefinedを返すので、呼び出し側は従来のIIFE方式にフォールバックする(#29)。
|
|
87
|
+
*/
|
|
88
|
+
splitModuleForStatementSplice(moduleName) {
|
|
89
|
+
const ast = this.moduleAST.get(moduleName);
|
|
90
|
+
const fileName = this.moduleNameAndFileName.get(moduleName);
|
|
91
|
+
if (!ast || !fileName) {
|
|
92
|
+
throw new Error(moduleName + " is not found");
|
|
93
|
+
}
|
|
94
|
+
return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parseAsStatementsAndFinalExpression(moduleName === this.entryModule);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 指定モジュールのRenameパス結果を返す。`renameAll`で事前に計算済みの
|
|
98
|
+
* ものをそのまま返すだけの参照用アクセサ。
|
|
99
|
+
*/
|
|
100
|
+
getRenameResult(moduleName) {
|
|
101
|
+
if (this.mode.rename === false) {
|
|
102
|
+
return NO_RENAME;
|
|
103
|
+
}
|
|
104
|
+
const cached = this.renameCache.get(moduleName);
|
|
105
|
+
if (!cached) {
|
|
106
|
+
throw new Error(moduleName + " is not found");
|
|
107
|
+
}
|
|
108
|
+
return cached;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Renameパス(#20): linkOrder(依存されている側が先)の順にモジュールごとの
|
|
112
|
+
* 短縮名を割り当てる。
|
|
113
|
+
*
|
|
114
|
+
* dofileやSLモードのrequireその場展開は、呼び出し元と同じLuaスコープに
|
|
115
|
+
* 関数で包まずに直接展開されるため、モジュールをまたいで同じ短縮名を
|
|
116
|
+
* 再利用すると本来無関係な変数同士が衝突しうる(#12)。これを安全に防ぐため、
|
|
117
|
+
* あるモジュールが実際に使った短縮名は、後続モジュールを処理する前に
|
|
118
|
+
* `identifiersInUse`(予約名の集合)へ積み増す。これにより短縮名は
|
|
119
|
+
* プログラム全体で重複しなくなる(モジュール間での再利用による圧縮は
|
|
120
|
+
* 犠牲になるが、モジュール内でのスコープに基づく再利用は維持される)。
|
|
121
|
+
*/
|
|
122
|
+
renameAll() {
|
|
123
|
+
if (this.mode.rename === false) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.linkOrder.forEach((moduleName) => {
|
|
127
|
+
const resolved = this.moduleResolve.get(moduleName);
|
|
128
|
+
if (!resolved) {
|
|
129
|
+
throw new Error(moduleName + " is not found");
|
|
130
|
+
}
|
|
131
|
+
const result = (0, renamer_1.assignRenames)(resolved, this.identifiersInUse, this.globalRenames);
|
|
132
|
+
this.renameCache.set(moduleName, result);
|
|
133
|
+
result.usedNames.forEach((name) => this.identifiersInUse.add(name));
|
|
134
|
+
});
|
|
72
135
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Transformパス(#8b, #9): モジュールごとにASTレベルの最適化を適用する。
|
|
138
|
+
* computeGlobalRenamesの後・renameAllの前に実行する必要がある(renameは
|
|
139
|
+
* このパスが確定させた最終的な文構造を前提に短縮名を割り当てるため)。
|
|
140
|
+
*
|
|
141
|
+
* 実行順序は 8b(エイリアス挿入)→ #9(local宣言のまとめ上げ)。
|
|
142
|
+
* 8bが挿入する複数の1変数1式local文は、#9のまとめ上げ対象としてそのまま
|
|
143
|
+
* 束ねられるため、8bを先に行うことで両者の効果が重なる。
|
|
144
|
+
*
|
|
145
|
+
* 8bは新しい識別子ノード(エイリアスの宣言と、書き換えられた参照)を生成する。
|
|
146
|
+
* これらのノードは元のResolveパス結果には存在しないため、#9のハザード1判定
|
|
147
|
+
* (「候補文がグループ内で宣言済みの変数を参照していないか」)が正しく働くには、
|
|
148
|
+
* 8bの直後・#9の直前でResolveパスを再実行しておく必要がある。この順序を
|
|
149
|
+
* 誤ると、8bがrequire等の頻出グローバルをエイリアス化した際に、そのエイリアス
|
|
150
|
+
* 宣言自体と「エイリアス経由で呼び出す側」の文を#9が誤って1つのlocal文に
|
|
151
|
+
* まとめてしまい(エイリアス変数がまだ束縛される前のスコープで参照される形に
|
|
152
|
+
* なり)、意味が壊れる(要修正が発覚した実例)。
|
|
153
|
+
*/
|
|
154
|
+
transformAll() {
|
|
155
|
+
// globalRenames.keys()は8aが実際にリネームした(=代入もされていた)名前のみ。
|
|
156
|
+
// neverRenameGlobalsは代入されていない名前にも及ぶ保護指定なので、8bのエイリアス化
|
|
157
|
+
// が誤ってそれらを書き換えてしまわないよう、必ず両方をあわせてexcludeNamesに渡す。
|
|
158
|
+
const excludeGlobalNames = new Set([
|
|
159
|
+
...this.globalRenames.keys(),
|
|
160
|
+
...(this.mode.neverRenameGlobals ?? []),
|
|
161
|
+
]);
|
|
162
|
+
this.linkOrder.forEach((moduleName) => {
|
|
163
|
+
const ast = this.moduleAST.get(moduleName);
|
|
164
|
+
let resolved = this.moduleResolve.get(moduleName);
|
|
165
|
+
if (!ast || !resolved) {
|
|
166
|
+
throw new Error(moduleName + " is not found");
|
|
80
167
|
}
|
|
168
|
+
if (this.mode.rename !== false && this.mode.globalAlias !== false) {
|
|
169
|
+
(0, transform_1.insertGlobalAliases)(ast, resolved, {
|
|
170
|
+
excludeNames: excludeGlobalNames,
|
|
171
|
+
});
|
|
172
|
+
resolved = (0, resolver_1.resolveScopes)(ast);
|
|
173
|
+
}
|
|
174
|
+
if (this.mode.mergeLocals !== false) {
|
|
175
|
+
(0, transform_1.mergeLocalDeclarations)(ast, resolved, {
|
|
176
|
+
preserveRequireSplice: !this.mode.moduleLikeLua,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this.moduleResolve.set(moduleName, resolved);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* #8aのGlobal Renameパス: リンクされた全モジュールを横断して、代入されている
|
|
184
|
+
* グローバル(neverRenameGlobalsに含まれるものを除く)に短縮名を割り当てる。
|
|
185
|
+
* グローバルは1つのランタイム束縛をモジュール間で共有するため、この判定・採番は
|
|
186
|
+
* renameAll(モジュールごとに独立して行うローカルのリネーム)より前に、
|
|
187
|
+
* 一度だけ行う必要がある。
|
|
188
|
+
*
|
|
189
|
+
* 選ばれた短縮名はrenameAllの前にidentifiersInUseへ予約し、元の長い名前の予約は
|
|
190
|
+
* 解除する(もう出力に現れないため)。順序を誤ると、
|
|
191
|
+
* ローカルの短縮名がグローバルの新しい短縮名と衝突しうる。
|
|
192
|
+
*/
|
|
193
|
+
computeGlobalRenames() {
|
|
194
|
+
if (this.mode.rename === false || this.mode.globalRename === false) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const neverRename = this.mode.neverRenameGlobals ?? new Set();
|
|
198
|
+
this.globalRenames = (0, globalRename_1.classifyAndRenameGlobals)(this.moduleResolve, neverRename, this.identifiersInUse);
|
|
199
|
+
this.globalRenames.forEach((shortName, originalName) => {
|
|
200
|
+
this.identifiersInUse.add(shortName);
|
|
201
|
+
this.identifiersInUse.delete(originalName);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
printModule(moduleName) {
|
|
205
|
+
const ast = this.moduleAST.get(moduleName);
|
|
206
|
+
const fileName = this.moduleNameAndFileName.get(moduleName);
|
|
207
|
+
if (!ast || !fileName) {
|
|
208
|
+
throw new Error(moduleName + " is not found");
|
|
81
209
|
}
|
|
82
|
-
|
|
210
|
+
return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parse(moduleName === this.entryModule);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* エントリファイルから到達可能な全モジュールをASTレベルで解決するLinkパス(#18)。
|
|
214
|
+
* - ファイルごとのパースは一度だけ行う(同一モジュールの多重require/dofileの重複排除)
|
|
215
|
+
* - require/dofileの参照グラフに循環があればエラーを投げる
|
|
216
|
+
* - 出力(Print)を開始する前に、必要なモジュール解決をすべて完了させる
|
|
217
|
+
*/
|
|
218
|
+
link() {
|
|
219
|
+
const visiting = new Set();
|
|
220
|
+
const stack = [];
|
|
221
|
+
const visit = (moduleName) => {
|
|
222
|
+
if (visiting.has(moduleName)) {
|
|
223
|
+
const cycleStart = stack.indexOf(moduleName);
|
|
224
|
+
const cycle = [...stack.slice(cycleStart), moduleName];
|
|
225
|
+
throw new Error("Circular require/dofile detected: " + cycle.join(" -> "));
|
|
226
|
+
}
|
|
227
|
+
if (this.moduleAST.has(moduleName)) {
|
|
228
|
+
// 解決済み(このモジュールは複数箇所から参照されていても一度しかパースしない)
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
visiting.add(moduleName);
|
|
232
|
+
stack.push(moduleName);
|
|
233
|
+
const fullResolvePath = path_1.default.join(this.dir, ...moduleName.split(".")) + ".lua";
|
|
234
|
+
if (!fs_1.default.existsSync(fullResolvePath)) {
|
|
235
|
+
throw new Error(moduleName + " is not found");
|
|
236
|
+
}
|
|
83
237
|
const code = fs_1.default.readFileSync(fullResolvePath).toString();
|
|
84
238
|
const ast = luaparse_1.default.parse(code, this.luaParseSettings);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
239
|
+
// Resolveパス(#19): このモジュールのスコープ/シンボルを解析し、Renameパスの
|
|
240
|
+
// 入力として使い回せるようキャッシュする。グローバル参照はプログラム全体で
|
|
241
|
+
// 予約すべき名前(identifiersInUse)としてここで集計する。
|
|
242
|
+
const resolved = (0, resolver_1.resolveScopes)(ast);
|
|
243
|
+
this.moduleResolve.set(moduleName, resolved);
|
|
244
|
+
resolved.globals.forEach((binding) => this.identifiersInUse.add(binding.name));
|
|
245
|
+
this.moduleSourceText.set(moduleName, code);
|
|
246
|
+
this.moduleAST.set(moduleName, ast);
|
|
247
|
+
// Source Mapの`sources`はURLとして解釈されるため、OS依存のpath.sepではなく
|
|
248
|
+
// 常に"/"区切りで保持する(Windows上でのビルドでも壊れないように)。
|
|
249
|
+
this.moduleNameAndFileName.set(moduleName, moduleName.replaceAll(".", "/") + ".lua");
|
|
250
|
+
(0, linker_1.findModuleReferences)(ast).forEach((ref) => {
|
|
251
|
+
visit(ref.moduleName);
|
|
252
|
+
});
|
|
253
|
+
visiting.delete(moduleName);
|
|
254
|
+
stack.pop();
|
|
255
|
+
this.linkOrder.push(moduleName);
|
|
256
|
+
};
|
|
257
|
+
visit(this.entryModule);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* require()(dofileは除く)で参照されているモジュール名の集合を求める。
|
|
261
|
+
* dofileは呼び出しごとに毎回展開しなおすため、キャッシュ/ホイストの対象にしない。
|
|
262
|
+
*/
|
|
263
|
+
collectRequireTargets() {
|
|
264
|
+
const targets = new Set();
|
|
265
|
+
this.linkOrder.forEach((moduleName) => {
|
|
266
|
+
const ast = this.moduleAST.get(moduleName);
|
|
267
|
+
if (!ast) {
|
|
268
|
+
return;
|
|
93
269
|
}
|
|
94
|
-
|
|
95
|
-
|
|
270
|
+
(0, linker_1.findModuleReferences)(ast).forEach((ref) => {
|
|
271
|
+
if (ref.kind === "require") {
|
|
272
|
+
targets.add(ref.moduleName);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
return targets;
|
|
277
|
+
}
|
|
278
|
+
buildRequireWrapper() {
|
|
279
|
+
const targets = this.collectRequireTargets();
|
|
280
|
+
const parts = [
|
|
281
|
+
"function require(m,r)package=package or{loaded={}};if package.loaded[m]then return package.loaded[m]end\n",
|
|
282
|
+
];
|
|
283
|
+
this.linkOrder.forEach((moduleName) => {
|
|
284
|
+
if (moduleName === this.entryModule || !targets.has(moduleName)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
parts.push('if m=="', moduleName, '"then r=(function() ', this.printModule(moduleName), " end)()end\n");
|
|
288
|
+
});
|
|
289
|
+
parts.push("package.loaded[m]=package.loaded[m]or r or true;return package.loaded[m]end\n");
|
|
290
|
+
return new source_map_1.SourceNode(null, null, null, parts);
|
|
96
291
|
}
|
|
97
292
|
}
|
|
98
293
|
exports.Minifier = Minifier;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildMinifiedOutput = buildMinifiedOutput;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
/**
|
|
9
|
+
* ミニファイ済みSourceNodeに、sourceMappingURLアノテーションとSource Mapを
|
|
10
|
+
* 付加した最終的な出力を組み立てる。アノテーションの出力形式は
|
|
11
|
+
* `BuildMinifiedOutputOptions.sourceMappingUrlStyle`で選択する。
|
|
12
|
+
*/
|
|
13
|
+
function buildMinifiedOutput(sourceNode, minFileName, mapFileName, options = {}) {
|
|
14
|
+
const style = options.sourceMappingUrlStyle ?? "legacy";
|
|
15
|
+
const marker = "//# sourceMappingURL=" + path_1.default.basename(mapFileName);
|
|
16
|
+
if (style === "legacy") {
|
|
17
|
+
// 旧storm-lua-minifyと完全に同じ出力(末尾に改行は付加しない)。
|
|
18
|
+
sourceNode.add("\n--[[\n" + marker + "\n]]");
|
|
19
|
+
}
|
|
20
|
+
else if (style === "line") {
|
|
21
|
+
sourceNode.add("\n-- " + marker + "\n");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
sourceNode.add("\n" + marker + "\n");
|
|
25
|
+
}
|
|
26
|
+
const sourceAndMap = sourceNode.toStringWithSourceMap({
|
|
27
|
+
file: path_1.default.basename(minFileName),
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
code: sourceAndMap.code,
|
|
31
|
+
map: JSON.stringify(sourceAndMap.map),
|
|
32
|
+
};
|
|
33
|
+
}
|