storm-lua-minify 0.1.0 → 0.1.1
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/LICENSE +1 -1
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/ast2lua.ts +36 -23
- package/src/cli.ts +28 -54
- package/src/minifier.ts +114 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2024 Nona Takahara, Mathias Bynens
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -3,3 +3,13 @@
|
|
|
3
3
|
このプログラムは [mathiasbynens/luamin](https://github.com/mathiasbynens/luamin) をベースにした Stormworks: Build and Rescue 向けのLua minifierです。
|
|
4
4
|
|
|
5
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の実際の挙動に近づけます
|
package/package.json
CHANGED
package/src/ast2lua.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
5
5
|
import Parser, { Comment } from "luaparse";
|
|
6
6
|
import { SourceNode } from "source-map";
|
|
7
|
+
import { Minifier, MinifierMode } from "./minifier";
|
|
7
8
|
|
|
8
9
|
export type Chunk = Parser.Chunk & {
|
|
9
10
|
globals?: (Parser.Base<"Identifer"> & {
|
|
@@ -100,9 +101,6 @@ const IDENTIFIER_PARTS = [
|
|
|
100
101
|
"_",
|
|
101
102
|
];
|
|
102
103
|
|
|
103
|
-
const identifierMap = new Map<string, string>();
|
|
104
|
-
const identifiersInUse = new Set<string>();
|
|
105
|
-
|
|
106
104
|
function wrapArray<T>(obj: T | T[]): T[] {
|
|
107
105
|
if (Array.isArray(obj)) {
|
|
108
106
|
return obj;
|
|
@@ -262,31 +260,33 @@ function insertSeparator(
|
|
|
262
260
|
return isNeedSeparator(a.toString(), b.toString()) ? separator : undefined;
|
|
263
261
|
}
|
|
264
262
|
|
|
265
|
-
export class
|
|
263
|
+
export class MinifyFile {
|
|
266
264
|
private fileName: string;
|
|
267
265
|
private ast: Chunk;
|
|
268
|
-
private
|
|
266
|
+
private minifier: Minifier;
|
|
267
|
+
private mode: MinifierMode;
|
|
269
268
|
|
|
270
269
|
constructor(
|
|
271
270
|
fileName: string,
|
|
272
271
|
ast: Chunk,
|
|
273
|
-
|
|
272
|
+
minifier: Minifier,
|
|
273
|
+
mode: MinifierMode
|
|
274
274
|
) {
|
|
275
275
|
this.fileName = fileName;
|
|
276
276
|
this.ast = ast;
|
|
277
|
-
this.
|
|
278
|
-
|
|
277
|
+
this.minifier = minifier;
|
|
278
|
+
this.mode = mode;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
parse() {
|
|
281
|
+
parse(noComment: boolean) {
|
|
282
282
|
const body = this.formatStatementList(this.ast.body);
|
|
283
|
-
|
|
283
|
+
if (!noComment && this.ast.comments) {
|
|
284
284
|
const comments = this.ast.comments as Comment[];
|
|
285
285
|
comments.reverse().filter(v => v.raw.includes("--#") || v.raw.includes("[[#")).forEach(comment => {
|
|
286
|
-
body.prepend(this.sourceNodeHelper(comment, comment.raw));
|
|
286
|
+
body.prepend([this.sourceNodeHelper(comment, comment.raw), "\n"]);
|
|
287
287
|
})
|
|
288
288
|
return body;
|
|
289
|
-
} else
|
|
289
|
+
} else {
|
|
290
290
|
return body;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
@@ -605,15 +605,28 @@ export class Minifier {
|
|
|
605
605
|
return result;
|
|
606
606
|
} else if (expression.type == "CallExpression") {
|
|
607
607
|
// requireの展開モードは2種類: SLモード-その場に読み込み, FLモード-require相当の関数で呼び出し
|
|
608
|
+
const callExpr = this.formatBase(expression.base).toString();
|
|
608
609
|
if (
|
|
609
|
-
|
|
610
|
+
(callExpr === "require" || callExpr === "dofile") &&
|
|
610
611
|
expression?.arguments[0].type === "StringLiteral"
|
|
611
612
|
) {
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
613
|
+
const moduleName = expression.arguments[0].raw
|
|
614
|
+
.replaceAll('"', "")
|
|
615
|
+
.replaceAll("'", "");
|
|
616
|
+
|
|
617
|
+
const res = this.minifier.parseModule(moduleName);
|
|
618
|
+
|
|
619
|
+
if (this.mode.moduleLikeLua) {
|
|
620
|
+
if (callExpr === "dofile") {
|
|
621
|
+
return (
|
|
622
|
+
res || this.sourceNodeHelper(undefined, "")
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
// requireならスキップ
|
|
626
|
+
} else {
|
|
627
|
+
return (
|
|
628
|
+
res || this.sourceNodeHelper(undefined, "")
|
|
629
|
+
);
|
|
617
630
|
}
|
|
618
631
|
}
|
|
619
632
|
const args = expression.arguments
|
|
@@ -759,7 +772,7 @@ export class Minifier {
|
|
|
759
772
|
return this.sourceNodeHelper(nameItem, "self", "self");
|
|
760
773
|
}
|
|
761
774
|
|
|
762
|
-
const defined = identifierMap.get(nameItem.name);
|
|
775
|
+
const defined = this.minifier.identifierMap.get(nameItem.name);
|
|
763
776
|
if (defined) {
|
|
764
777
|
return this.sourceNodeHelper(nameItem, defined, nameItem.name); // 第3引数は要調査
|
|
765
778
|
}
|
|
@@ -778,20 +791,20 @@ export class Minifier {
|
|
|
778
791
|
generateZeroes(length - (position + 1));
|
|
779
792
|
if (
|
|
780
793
|
isKeyword(this.currentIdentifier) ||
|
|
781
|
-
identifiersInUse.has(this.currentIdentifier)
|
|
794
|
+
this.minifier.identifiersInUse.has(this.currentIdentifier)
|
|
782
795
|
) {
|
|
783
796
|
return this.generateIdentifier(nameItem, nested);
|
|
784
797
|
}
|
|
785
|
-
identifierMap.set(nameItem.name, this.currentIdentifier);
|
|
798
|
+
this.minifier.identifierMap.set(nameItem.name, this.currentIdentifier);
|
|
786
799
|
return this.generateIdentifier(nameItem, nested);
|
|
787
800
|
}
|
|
788
801
|
--position;
|
|
789
802
|
}
|
|
790
803
|
this.currentIdentifier = "a" + generateZeroes(length);
|
|
791
|
-
if (identifiersInUse.has(this.currentIdentifier)) {
|
|
804
|
+
if (this.minifier.identifiersInUse.has(this.currentIdentifier)) {
|
|
792
805
|
return this.generateIdentifier(nameItem, nested);
|
|
793
806
|
}
|
|
794
|
-
identifierMap.set(nameItem.name, this.currentIdentifier);
|
|
807
|
+
this.minifier.identifierMap.set(nameItem.name, this.currentIdentifier);
|
|
795
808
|
return this.generateIdentifier(nameItem, nested);
|
|
796
809
|
|
|
797
810
|
// return this.sourceNodeHelper(nameItem, nameItem.name, nested ? nameItem.name : undefined);
|
package/src/cli.ts
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { Options } from "luaparse";
|
|
7
|
+
import { Minifier, MinifierMode } from "./minifier";
|
|
8
8
|
|
|
9
9
|
const program = new Command();
|
|
10
10
|
|
|
11
|
-
program
|
|
11
|
+
program
|
|
12
|
+
.version("0.1.1")
|
|
13
|
+
.description("A Lua minifier also outputs source map")
|
|
14
|
+
.option(
|
|
15
|
+
"-m, --module-like-lua",
|
|
16
|
+
"require・dofileの動作を実際のLuaに近づけます"
|
|
17
|
+
);
|
|
12
18
|
|
|
13
19
|
program.parse(process.argv);
|
|
14
20
|
|
|
@@ -21,61 +27,29 @@ const luaparseSetting: Partial<Options> = {
|
|
|
21
27
|
scope: true,
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
const mode: MinifierMode = program.opts();
|
|
31
|
+
|
|
24
32
|
luaFiles.forEach((fileName) => {
|
|
25
|
-
const includes = new Map<string, string>();
|
|
26
33
|
const parsedFileName = path.parse(fileName);
|
|
27
34
|
|
|
28
|
-
function requireHelper(recursiveFilePath: string) {
|
|
29
|
-
const resolvePath = path.relative(
|
|
30
|
-
parsedFileName.dir,
|
|
31
|
-
path.join(
|
|
32
|
-
parsedFileName.dir,
|
|
33
|
-
recursiveFilePath.replaceAll(".", path.sep) + ".lua"
|
|
34
|
-
)
|
|
35
|
-
);
|
|
36
|
-
const fullResolvePath = path.join(parsedFileName.dir, resolvePath);
|
|
37
|
-
|
|
38
|
-
if (!includes.has(resolvePath) && fs.existsSync(fullResolvePath)) {
|
|
39
|
-
const code = fs.readFileSync(fullResolvePath).toString();
|
|
40
|
-
const ast = Parser.parse(code, luaparseSetting) as Chunk;
|
|
41
|
-
if ("globals" in ast) {
|
|
42
|
-
includes.set(resolvePath, code);
|
|
43
|
-
return new Minifier(resolvePath, ast, requireHelper).parse();
|
|
44
|
-
}
|
|
45
|
-
includes.set(resolvePath, "");
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
35
|
if (fs.existsSync(fileName)) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
dir: parsedFileName.dir,
|
|
69
|
-
name: parsedFileName.name,
|
|
70
|
-
ext: parsedFileName.ext + ".map",
|
|
71
|
-
});
|
|
72
|
-
map.add("\n--[[\n//# sourceMappingURL=" + mapFileName + "\n]]");
|
|
73
|
-
|
|
74
|
-
const sourceAndMap = map.toStringWithSourceMap();
|
|
75
|
-
|
|
76
|
-
fs.writeFileSync(minFileName, sourceAndMap.code);
|
|
77
|
-
fs.writeFileSync(mapFileName, JSON.stringify(sourceAndMap.map));
|
|
78
|
-
}
|
|
36
|
+
const map = new Minifier(fileName, luaparseSetting, mode).parse();
|
|
37
|
+
const minFileName = path.format({
|
|
38
|
+
dir: parsedFileName.dir,
|
|
39
|
+
name: parsedFileName.name + ".min",
|
|
40
|
+
ext: ".lua",
|
|
41
|
+
});
|
|
42
|
+
const mapFileName = path.format({
|
|
43
|
+
dir: parsedFileName.dir,
|
|
44
|
+
name: parsedFileName.name,
|
|
45
|
+
ext: parsedFileName.ext + ".map",
|
|
46
|
+
});
|
|
47
|
+
map.add("\n--[[\n//# sourceMappingURL=" + mapFileName + "\n]]");
|
|
48
|
+
|
|
49
|
+
const sourceAndMap = map.toStringWithSourceMap();
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(minFileName, sourceAndMap.code);
|
|
52
|
+
fs.writeFileSync(mapFileName, JSON.stringify(sourceAndMap.map));
|
|
79
53
|
} else {
|
|
80
54
|
console.error("No such file: " + fileName);
|
|
81
55
|
}
|
package/src/minifier.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Parser, { Comment, 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
|
+
|
|
7
|
+
export interface MinifierMode {
|
|
8
|
+
moduleLikeLua: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Minifier {
|
|
12
|
+
readonly identifierMap: Map<string, string>;
|
|
13
|
+
readonly identifiersInUse: Set<string>;
|
|
14
|
+
readonly moduleSourceText: Map<string, string>;
|
|
15
|
+
readonly moduleSourceNode: Map<string, SourceNode>;
|
|
16
|
+
readonly moduleAST: Map<string, Chunk>;
|
|
17
|
+
readonly dir: string;
|
|
18
|
+
readonly entryModule: string;
|
|
19
|
+
readonly mode: MinifierMode;
|
|
20
|
+
readonly luaParseSettings: Partial<Options>;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
entryFilePath: string,
|
|
24
|
+
luaParseSettings: Partial<Options>,
|
|
25
|
+
mode: MinifierMode
|
|
26
|
+
) {
|
|
27
|
+
this.identifierMap = new Map<string, string>();
|
|
28
|
+
this.identifiersInUse = new Set<string>();
|
|
29
|
+
this.moduleSourceText = new Map<string, string>();
|
|
30
|
+
this.moduleSourceNode = new Map<string, SourceNode>();
|
|
31
|
+
this.moduleAST = new Map<string, Chunk>();
|
|
32
|
+
this.luaParseSettings = luaParseSettings;
|
|
33
|
+
this.mode = mode;
|
|
34
|
+
const pn = path.parse(entryFilePath);
|
|
35
|
+
this.dir = pn.dir;
|
|
36
|
+
this.entryModule = pn.name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parse() {
|
|
40
|
+
const sn = this.parseModule(this.entryModule);
|
|
41
|
+
|
|
42
|
+
if (this.mode.moduleLikeLua) {
|
|
43
|
+
// require関数を作成し、流し込む
|
|
44
|
+
/*
|
|
45
|
+
function require(m,r)
|
|
46
|
+
package=package or {loaded={}}
|
|
47
|
+
if package.loaded[m] then return package.loaded[m] end
|
|
48
|
+
if m=="<MODULE_NAME>" then r=(function()[[MODULE]]end)() end
|
|
49
|
+
package.loaded[m]=package.loaded[m] or r or true;return package.loaded[m]
|
|
50
|
+
end
|
|
51
|
+
*/
|
|
52
|
+
sn.prepend("package.loaded[m]=package.loaded[m]or r or true;return package.loaded[m]end\n");
|
|
53
|
+
this.moduleSourceNode.forEach((v, k) => {
|
|
54
|
+
if (k !== this.entryModule) {
|
|
55
|
+
sn.prepend(["if m==\"", k, "\"then r=(function() ", v ," end)()end\n"]);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
sn.prepend("function require(m,r)package=package or{loaded={}};if package.loaded[m]then return package.loaded[m]end\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// コメントの流し込み
|
|
62
|
+
const comments = this.moduleAST.get(this.entryModule)?.comments as
|
|
63
|
+
| Comment[]
|
|
64
|
+
| undefined;
|
|
65
|
+
if (comments) {
|
|
66
|
+
comments
|
|
67
|
+
.reverse()
|
|
68
|
+
.filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
|
|
69
|
+
.forEach((comment) => {
|
|
70
|
+
sn.prepend([
|
|
71
|
+
new SourceNode(
|
|
72
|
+
comment.loc?.start.line || null,
|
|
73
|
+
comment.loc?.start.column || null,
|
|
74
|
+
this.entryModule, // 本当に自分のファイル名でよいかは要検討
|
|
75
|
+
comment.raw
|
|
76
|
+
)
|
|
77
|
+
,"\n"]);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return sn;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
parseModule(moduleName: string): SourceNode {
|
|
85
|
+
const resolvePath = moduleName.replaceAll(".", path.sep) + ".lua";
|
|
86
|
+
const fullResolvePath = path.join(this.dir, resolvePath);
|
|
87
|
+
|
|
88
|
+
if (this.moduleSourceNode.has(moduleName)) {
|
|
89
|
+
const res = this.moduleSourceNode.get(moduleName);
|
|
90
|
+
if (res) {
|
|
91
|
+
return res;
|
|
92
|
+
}
|
|
93
|
+
} else if (fs.existsSync(fullResolvePath)) {
|
|
94
|
+
const code = fs.readFileSync(fullResolvePath).toString();
|
|
95
|
+
const ast = Parser.parse(code, this.luaParseSettings) as Chunk;
|
|
96
|
+
if ("globals" in ast) {
|
|
97
|
+
ast.globals?.map((v) => this.identifiersInUse.add(v.name));
|
|
98
|
+
|
|
99
|
+
const sourceNode = new MinifyFile(
|
|
100
|
+
resolvePath,
|
|
101
|
+
ast,
|
|
102
|
+
this,
|
|
103
|
+
this.mode
|
|
104
|
+
).parse(resolvePath === this.entryModule);
|
|
105
|
+
|
|
106
|
+
this.moduleSourceText.set(moduleName, code);
|
|
107
|
+
this.moduleAST.set(moduleName, ast);
|
|
108
|
+
this.moduleSourceNode.set(moduleName, sourceNode);
|
|
109
|
+
return sourceNode;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw new Error(moduleName + " is not found");
|
|
113
|
+
}
|
|
114
|
+
}
|