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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) Mathias Bynens, 2024 Nona Takahara
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "storm-lua-minify",
3
3
  "description": "A Lua minifier also outputs source map",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "dependencies": {
6
6
  "commander": "^12.1.0",
7
7
  "luamin": "^1.0.4",
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 Minifier {
263
+ export class MinifyFile {
266
264
  private fileName: string;
267
265
  private ast: Chunk;
268
- private requireHelper: (fileName: string) => SourceNode | undefined;
266
+ private minifier: Minifier;
267
+ private mode: MinifierMode;
269
268
 
270
269
  constructor(
271
270
  fileName: string,
272
271
  ast: Chunk,
273
- requireHelper: (fileName: string) => SourceNode | undefined
272
+ minifier: Minifier,
273
+ mode: MinifierMode
274
274
  ) {
275
275
  this.fileName = fileName;
276
276
  this.ast = ast;
277
- this.requireHelper = requireHelper;
278
- ast.globals?.map((v) => identifiersInUse.add(v.name));
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
- /*if (this.ast.comments) {
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
- this.formatBase(expression.base).toString() == "require" &&
610
+ (callExpr === "require" || callExpr === "dofile") &&
610
611
  expression?.arguments[0].type === "StringLiteral"
611
612
  ) {
612
- const res = this.requireHelper(
613
- expression.arguments[0].raw.replaceAll('"', "")
614
- );
615
- if (res != undefined) {
616
- return res;
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 Parser, { Options } from "luaparse";
7
- import { Chunk, Minifier } from "./ast2lua";
6
+ import { Options } from "luaparse";
7
+ import { Minifier, MinifierMode } from "./minifier";
8
8
 
9
9
  const program = new Command();
10
10
 
11
- program.version("0.1.0").description("A Lua minifier also outputs source map");
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 code = fs.readFileSync(fileName).toString();
52
- const ast = Parser.parse(code, luaparseSetting) as Chunk;
53
- if ("globals" in ast) {
54
- includes.set(parsedFileName.base, code);
55
- const map = new Minifier(parsedFileName.base, ast, requireHelper).parse();
56
-
57
- includes.forEach((v, k) => {
58
- console.log(k);
59
- map.setSourceContent(k, v);
60
- });
61
-
62
- const minFileName = path.format({
63
- dir: parsedFileName.dir,
64
- name: parsedFileName.name + ".min",
65
- ext: ".lua",
66
- });
67
- const mapFileName = path.format({
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
  }
@@ -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
+ }