+2
-1
packages/core/package.json
+2
-1
packages/core/package.json
+16
-12
packages/core/src/patch.ts
+16
-12
packages/core/src/patch.ts
···
78
78
for (const [id, func] of Object.entries(entry)) {
79
79
if (!Object.hasOwn(moduleCache, id) && func.__moonlight !== true) {
80
80
moduleCache[id] = func.toString().replace(/\n/g, "");
81
+
moonlight.moonmap.parseScript(id, moduleCache[id]);
81
82
}
82
83
}
83
84
···
162
163
const parsed = moonlight.lunast.parseScript(id, moduleString);
163
164
if (parsed != null) {
164
165
for (const [parsedId, parsedScript] of Object.entries(parsed)) {
165
-
// parseScript adds an extra ; for some reason
166
-
const fixedScript = parsedScript
167
-
.trimEnd()
168
-
.substring(0, parsedScript.lastIndexOf(";"));
169
-
170
-
if (patchModule(parsedId, "lunast", fixedScript)) {
171
-
moduleCache[parsedId] = fixedScript;
166
+
if (patchModule(parsedId, "lunast", parsedScript)) {
167
+
moduleCache[parsedId] = parsedScript;
172
168
}
173
169
}
174
170
}
···
238
234
const entrypoints: string[] = [];
239
235
let inject = false;
240
236
237
+
for (const [name, func] of Object.entries(
238
+
moonlight.moonmap.getWebpackModules("window.moonlight.moonmap")
239
+
)) {
240
+
modules[name] = func;
241
+
inject = true;
242
+
}
243
+
241
244
for (const [_modId, mod] of Object.entries(entry)) {
242
245
const modStr = mod.toString();
243
246
for (const wpModule of webpackModules) {
···
300
303
interface Window {
301
304
webpackChunkdiscord_app: WebpackJsonp;
302
305
}
306
+
}
307
+
308
+
function moduleSourceGetter(id: string) {
309
+
return moduleCache[id] ?? null;
303
310
}
304
311
305
312
/*
···
313
320
export async function installWebpackPatcher() {
314
321
await handleModuleDependencies();
315
322
316
-
moonlight.lunast.setModuleSourceGetter((id) => {
317
-
return moduleCache[id] ?? null;
318
-
});
323
+
moonlight.lunast.setModuleSourceGetter(moduleSourceGetter);
324
+
moonlight.moonmap.setModuleSourceGetter(moduleSourceGetter);
319
325
320
326
let realWebpackJsonp: WebpackJsonp | null = null;
321
327
Object.defineProperty(window, "webpackChunkdiscord_app", {
···
375
381
window.webpackChunkdiscord_app = [];
376
382
injectModules(modules);
377
383
}
378
-
379
-
moonlight.lunast.setDefaultRequire(this);
380
384
381
385
Object.defineProperty(this, "m", {
382
386
value: modules,
-131
packages/lunast/README.md
-131
packages/lunast/README.md
···
1
-
# LunAST
2
-
3
-
LunAST is an experimental in-development [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree)-based remapper and patcher for Webpack modules.
4
-
5
-
## Introduction
6
-
7
-
Modern Webpack patching functions off of matching existing minified code (using a string or regular expression) and then replacing it. While this is an easy to use and powerful way of patching Webpack modules, there are many downsides:
8
-
9
-
- Even the smallest change can break patches, which can require lots of maintenance, especially on large Discord bundler changes.
10
-
- Fetching exports from a Webpack module will sometimes result in minified export names. These exports must be manually remapped to human readable names by a library extension.
11
-
- Making complicated patches is extremely difficult and means your patch has more points of failure.
12
-
13
-
To solve this, LunAST generates [the ESTree format](https://github.com/estree/estree) with a handful of libraries ([meriyah](https://github.com/meriyah/meriyah), [estree-toolkit](https://github.com/sarsamurmu/estree-toolkit), [astring](https://github.com/davidbonnet/astring)) on each Webpack module. This makes large-scale manipulation and mapping feasible, by allowing you to write code to detect what modules you want to find.
14
-
15
-
## Usage
16
-
17
-
### Embedding into your own code
18
-
19
-
LunAST is not ready to be used in other projects just yet. In the future, LunAST will be a standalone library.
20
-
21
-
### Registering a processor
22
-
23
-
LunAST functions off of "processors". Processors have a unique ID, an optional filter (string or regex) on what to parse, and a process function which receives the AST.
24
-
25
-
The process function returns a boolean, which when true will unregister the processor. Once you have found what you're looking for, you can return true to skip parsing any other subsequent module, speeding up load times.
26
-
27
-
LunAST includes some core processors, and extensions can register their own processors (citation needed).
28
-
29
-
```ts
30
-
register({
31
-
name: "UniqueIDForTheProcessorSystem",
32
-
find: "some string or regex to search for", // optional
33
-
priority: 0, // optional
34
-
process({ id, ast, lunast }) {
35
-
// do some stuff with the ast
36
-
return false; // return true to unregister
37
-
}
38
-
});
39
-
```
40
-
41
-
### Mapping with LunAST
42
-
43
-
LunAST can use proxies to remap minified export names to more human readable ones. Let's say that you determined the module ID and export name of a component you want in a module.
44
-
45
-
First, you must define the type. A type contains a unique name and a list of fields. These fields contain the minified name and the human-readable name that can be used in code.
46
-
47
-
Then, register the module, with the ID passed to you in the process function. Specify its type so the remapper knows what fields to remap. It is suggested to name the module and type with the same name.
48
-
49
-
```ts
50
-
process({ id, ast, lunast }) {
51
-
let exportName: string | null = null;
52
-
53
-
// some code to discover the export name...
54
-
55
-
if (exportName != null) {
56
-
lunast.addType({
57
-
name: "SomeModule",
58
-
fields: [
59
-
{
60
-
name: "SomeComponent",
61
-
unmapped: exportName
62
-
}
63
-
]
64
-
});
65
-
lunast.addModule({
66
-
name: "SomeModule",
67
-
id,
68
-
type: "SomeModule"
69
-
});
70
-
return true;
71
-
}
72
-
73
-
return false;
74
-
}
75
-
```
76
-
77
-
Then, you need to specify the type of the module in `types.ts`. Using the `import` statement in Webpack modules is not supported yet. Hopefully this step is automated in the future.
78
-
79
-
After all this, fetch the remapped module and its remapped field:
80
-
81
-
```ts
82
-
moonlight.lunast.remap("SomeModule").SomeComponent
83
-
```
84
-
85
-
### Patching with LunAST
86
-
87
-
LunAST also enables you to modify the AST and then rebuild a module string from the modified AST. It is suggested you read the [estree-toolkit](https://estree-toolkit.netlify.app/welcome) documentation.
88
-
89
-
You can use the `magicAST` function to turn some JavaScript code into another AST node, and then merge/replace the original AST.
90
-
91
-
**After you modify the AST, call the markDirty function.** LunAST will not know to replace the module otherwise.
92
-
93
-
```ts
94
-
process({ ast, markDirty }) {
95
-
const node = /* do something with the AST */;
96
-
if (node != null) {
97
-
const replacement = magicAST("return 1 + 1");
98
-
node.replaceWith(replacement);
99
-
markDirty();
100
-
return true;
101
-
}
102
-
103
-
return false;
104
-
}
105
-
```
106
-
107
-
## FAQ
108
-
109
-
### How do you fetch the scripts to parse?
110
-
111
-
Fetching the content of the `<script>` tags is impossible, and making a `fetch` request would result in different headers to what the client would normally send. We use `Function.prototype.toString()` and wrap the function in parentheses to ensure the anonymous function is valid JavaScript.
112
-
113
-
### Isn't this slow?
114
-
115
-
Not really. LunAST runs in roughly ~10ms on [my](https://github.com/NotNite) machine, with filtering for what modules to parse. Parsing every module takes only a second. There are future plans to cache and parallelize the process, so that load times are only slow once.
116
-
117
-
You can measure how long LunAST took to process with the `moonlight.lunast.elapsed` variable.
118
-
119
-
### Does this mean patches are dead?
120
-
121
-
No. Patches will continue to serve their purpose and be supported in moonlight, no matter what. LunAST should also work with patches, but patches may conflict or not match.
122
-
123
-
[astring](https://github.com/davidbonnet/astring) may need to be forked in the future to output code without whitespace, in the event patches fail to match on AST-patched code.
124
-
125
-
### This API surface seems kind of bad
126
-
127
-
This is still in heavy development and all suggestions on how to improve it are welcome. :3
128
-
129
-
### Can I help?
130
-
131
-
Discussion takes place in the [moonlight Discord server](https://discord.gg/FdZBTFCP6F) and its `#lunast-devel` channel.
-22
packages/lunast/TODO.md
-22
packages/lunast/TODO.md
···
1
-
# LunAST TODO
2
-
3
-
- [ ] Experiment more! We need to know what's bad with this
4
-
- [ ] Write utility functions for imports, exports, etc.
5
-
- [ ] Imports
6
-
- [x] Exports
7
-
- [ ] Constant bindings for an object
8
-
- [ ] Map Z/ZP to default
9
-
- [x] Steal Webpack require and use it in our LunAST instance
10
-
- [ ] Map `import` statements to LunAST
11
-
- [x] Support patching in the AST
12
-
- Let user modify the AST, have a function to flag it as modified, if it's modified we serialize it back into a string and put it back into Webpack
13
-
- We already have a `priority` system for this
14
-
- [ ] Run in parallel with service workers
15
-
- This is gonna require making Webpack entrypoint async and us doing kickoff ourselves
16
-
- [ ] Support lazy loaded chunks
17
-
- Works right now, but will break when caching is implemented
18
-
- [ ] Split into a new repo on GitHub, publish to NPM maybe
19
-
- [ ] Implement caching based off of the client build and LunAST commit
20
-
- Means you only have to have a long client start once per client build
21
-
- [ ] Process in CI to use if available on startup
22
-
- Should mean, if you're lucky, client starts only take the extra time to make the request
-14
packages/lunast/package.json
-14
packages/lunast/package.json
···
1
-
{
2
-
"name": "@moonlight-mod/lunast",
3
-
"version": "1.0.0",
4
-
"main": "./src/index.ts",
5
-
"types": "./src/index.ts",
6
-
"exports": {
7
-
".": "./src/index.ts"
8
-
},
9
-
"dependencies": {
10
-
"astring": "^1.9.0",
11
-
"estree-toolkit": "^1.7.8",
12
-
"meriyah": "^6.0.1"
13
-
}
14
-
}
-196
packages/lunast/src/index.ts
-196
packages/lunast/src/index.ts
···
1
-
import { RemapField, RemapModule, RemapType } from "./types";
2
-
import { Remapped } from "./modules";
3
-
import { getProcessors, parseFixed } from "./utils";
4
-
import { Processor, ProcessorState } from "./remap";
5
-
import { generate } from "astring";
6
-
7
-
export default class LunAST {
8
-
private modules: Record<string, RemapModule>;
9
-
private types: Record<string, RemapType>;
10
-
private successful: Set<string>;
11
-
12
-
private typeCache: Record<string, RemapType | null>;
13
-
private fieldCache: Record<
14
-
string,
15
-
Record<string | symbol, RemapField | null>
16
-
>;
17
-
private processors: Processor[];
18
-
private defaultRequire?: (id: string) => any;
19
-
private getModuleSource?: (id: string) => string;
20
-
21
-
elapsed: number;
22
-
23
-
constructor() {
24
-
this.modules = {};
25
-
this.types = {};
26
-
this.successful = new Set();
27
-
28
-
this.typeCache = {};
29
-
this.fieldCache = {};
30
-
this.processors = getProcessors();
31
-
32
-
this.elapsed = 0;
33
-
}
34
-
35
-
public static getVersion() {
36
-
// TODO: embed version in build when we move this to a new repo
37
-
// this is here for caching based off of the lunast commit ID
38
-
return "dev";
39
-
}
40
-
41
-
public parseScript(id: string, code: string): Record<string, string> {
42
-
const start = performance.now();
43
-
44
-
const available = [...this.processors]
45
-
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
46
-
.filter((x) => {
47
-
if (x.find == null) return true;
48
-
const finds = Array.isArray(x.find) ? x.find : [x.find];
49
-
return finds.every((find) =>
50
-
typeof find === "string" ? code.indexOf(find) !== -1 : find.test(code)
51
-
);
52
-
})
53
-
.filter((x) => x.manual !== true);
54
-
55
-
const ret = this.parseScriptInternal(id, code, available);
56
-
57
-
const end = performance.now();
58
-
this.elapsed += end - start;
59
-
60
-
return ret;
61
-
}
62
-
63
-
// This is like this so processors can trigger other processors while they're parsing
64
-
private parseScriptInternal(
65
-
id: string,
66
-
code: string,
67
-
processors: Processor[]
68
-
) {
69
-
const ret: Record<string, string> = {};
70
-
if (processors.length === 0) return ret;
71
-
72
-
// Wrap so the anonymous function is valid JS
73
-
const module = parseFixed(`(\n${code}\n)`);
74
-
let dirty = false;
75
-
const state: ProcessorState = {
76
-
id,
77
-
ast: module,
78
-
lunast: this,
79
-
markDirty: () => {
80
-
dirty = true;
81
-
},
82
-
trigger: (id, tag) => {
83
-
const source = this.getModuleSourceById(id);
84
-
if (source == null) return;
85
-
if (this.successful.has(tag)) return;
86
-
const processor = this.processors.find((x) => x.name === tag);
87
-
if (processor == null) return;
88
-
const theirRet = this.parseScriptInternal(id, source, [processor]);
89
-
Object.assign(ret, theirRet);
90
-
}
91
-
};
92
-
93
-
for (const processor of processors) {
94
-
if (processor.process(state)) {
95
-
this.processors.splice(this.processors.indexOf(processor), 1);
96
-
this.successful.add(processor.name);
97
-
}
98
-
}
99
-
100
-
const str = dirty ? generate(module) : null;
101
-
if (str != null) ret[id] = str;
102
-
103
-
return ret;
104
-
}
105
-
106
-
public getType(name: string) {
107
-
return (
108
-
this.typeCache[name] ?? (this.typeCache[name] = this.types[name] ?? null)
109
-
);
110
-
}
111
-
112
-
public getIdForModule(name: string) {
113
-
return Object.values(this.modules).find((x) => x.name === name)?.id ?? null;
114
-
}
115
-
116
-
public addModule(module: RemapModule) {
117
-
if (!this.modules[module.name]) {
118
-
this.modules[module.name] = module;
119
-
} else {
120
-
throw new Error(
121
-
`Module ${module.name} already registered (${
122
-
this.modules[module.name].id
123
-
})`
124
-
);
125
-
}
126
-
}
127
-
128
-
public addType(type: RemapType) {
129
-
if (!this.types[type.name]) {
130
-
this.types[type.name] = type;
131
-
} else {
132
-
throw new Error(`Type ${type.name} already registered`);
133
-
}
134
-
}
135
-
136
-
public proxy(obj: any, type: RemapType): any {
137
-
const fields =
138
-
this.fieldCache[type.name] ?? (this.fieldCache[type.name] = {});
139
-
140
-
return new Proxy(obj, {
141
-
get: (target, prop) => {
142
-
const field =
143
-
fields[prop] ??
144
-
(fields[prop] = type.fields.find((x) => x.name === prop) ?? null);
145
-
if (field) {
146
-
const fieldType =
147
-
field.type != null ? this.getType(field.type) : null;
148
-
const name = field.unmapped ?? field.name;
149
-
if (fieldType != null) {
150
-
return this.proxy(target[name], fieldType);
151
-
} else {
152
-
return target[name];
153
-
}
154
-
} else {
155
-
return target[prop];
156
-
}
157
-
}
158
-
});
159
-
}
160
-
161
-
// TODO: call this with require we obtain from the webpack entrypoint
162
-
public setDefaultRequire(require: (id: string) => any) {
163
-
this.defaultRequire = require;
164
-
}
165
-
166
-
public setModuleSourceGetter(getSource: (id: string) => string) {
167
-
this.getModuleSource = getSource;
168
-
}
169
-
170
-
public getModuleSourceById(id: string) {
171
-
return this.getModuleSource?.(id) ?? null;
172
-
}
173
-
174
-
public remap<Id extends keyof Remapped>(
175
-
id: Id,
176
-
require?: (id: string) => any
177
-
): Remapped[Id] | null {
178
-
const mappedModule = this.modules[id];
179
-
if (!mappedModule) return null;
180
-
181
-
const realRequire = require ?? this.defaultRequire;
182
-
if (!realRequire) return null;
183
-
184
-
const module = realRequire(mappedModule.id);
185
-
if (module == null) return null;
186
-
187
-
const type = this.getType(mappedModule.type);
188
-
if (type != null) {
189
-
return this.proxy(module, type);
190
-
} else {
191
-
return module;
192
-
}
193
-
}
194
-
}
195
-
196
-
export { Remapped } from "./modules";
-4
packages/lunast/src/modules.ts
-4
packages/lunast/src/modules.ts
-170
packages/lunast/src/modules/test.ts
-170
packages/lunast/src/modules/test.ts
···
1
-
import { traverse, is } from "estree-toolkit";
2
-
import { getPropertyGetters, register, magicAST, getImports } from "../utils";
3
-
import { BlockStatement } from "estree-toolkit/dist/generated/types";
4
-
5
-
// These aren't actual modules yet, I'm just using this as a testbed for stuff
6
-
7
-
// Exports example
8
-
/*register({
9
-
name: "ApplicationStoreDirectoryStore",
10
-
find: '"displayName","ApplicationStoreDirectoryStore"',
11
-
process({ ast }) {
12
-
const exports = getExports(ast);
13
-
return Object.keys(exports).length > 0;
14
-
}
15
-
});
16
-
17
-
register({
18
-
name: "FluxDispatcher",
19
-
find: "addBreadcrumb:",
20
-
process({ id, ast, lunast }) {
21
-
const exports = getExports(ast);
22
-
for (const [name, data] of Object.entries(exports)) {
23
-
if (!is.identifier(data.argument)) continue;
24
-
const binding = data.scope.getOwnBinding(data.argument.name);
25
-
console.log(name, binding);
26
-
}
27
-
return false;
28
-
}
29
-
});*/
30
-
31
-
// Patching example
32
-
register({
33
-
name: "ImagePreview",
34
-
find: ".Messages.OPEN_IN_BROWSER",
35
-
process({ id, ast, lunast, markDirty }) {
36
-
const getters = getPropertyGetters(ast);
37
-
const replacement = magicAST(`return require("common_react").createElement(
38
-
"div",
39
-
{
40
-
style: {
41
-
color: "white",
42
-
},
43
-
},
44
-
"balls"
45
-
)`)!;
46
-
for (const data of Object.values(getters)) {
47
-
if (!is.identifier(data.expression)) continue;
48
-
49
-
const node = data.scope.getOwnBinding(data.expression.name);
50
-
if (!node) continue;
51
-
52
-
const body = node.path.get<BlockStatement>("body");
53
-
body.replaceWith(replacement);
54
-
}
55
-
markDirty();
56
-
57
-
return true;
58
-
}
59
-
});
60
-
61
-
// Remapping example
62
-
register({
63
-
name: "ClipboardUtils",
64
-
find: 'document.queryCommandEnabled("copy")',
65
-
process({ id, ast, lunast }) {
66
-
const getters = getPropertyGetters(ast);
67
-
const fields = [];
68
-
69
-
for (const [name, data] of Object.entries(getters)) {
70
-
if (!is.identifier(data.expression)) continue;
71
-
const node = data.scope.getOwnBinding(data.expression.name);
72
-
if (!node) continue;
73
-
74
-
let isSupportsCopy = false;
75
-
traverse(node.path.node!, {
76
-
MemberExpression(path) {
77
-
if (
78
-
is.identifier(path.node?.property) &&
79
-
path.node?.property.name === "queryCommandEnabled"
80
-
) {
81
-
isSupportsCopy = true;
82
-
this.stop();
83
-
}
84
-
}
85
-
});
86
-
87
-
if (isSupportsCopy) {
88
-
fields.push({
89
-
name: "SUPPORTS_COPY",
90
-
unmapped: name
91
-
});
92
-
} else {
93
-
fields.push({
94
-
name: "copy",
95
-
unmapped: name
96
-
});
97
-
}
98
-
}
99
-
100
-
if (fields.length > 0) {
101
-
lunast.addType({
102
-
name: "ClipboardUtils",
103
-
fields
104
-
});
105
-
lunast.addModule({
106
-
name: "ClipboardUtils",
107
-
id,
108
-
type: "ClipboardUtils"
109
-
});
110
-
return true;
111
-
}
112
-
113
-
return false;
114
-
}
115
-
});
116
-
117
-
// Parse all modules to demonstrate speed loss
118
-
/*register({
119
-
name: "AllModules",
120
-
process({ id, ast, lunast }) {
121
-
return false;
122
-
}
123
-
});*/
124
-
125
-
// Triggering a processor from another processor
126
-
register({
127
-
name: "FluxDispatcherParent",
128
-
find: ["isDispatching", "dispatch", "googlebot"],
129
-
process({ id, ast, lunast, trigger }) {
130
-
const imports = getImports(ast);
131
-
// This is so stupid lol
132
-
const usages = Object.entries(imports)
133
-
.map(([name, data]): [string, number] => {
134
-
if (!is.identifier(data.expression)) return [name, 0];
135
-
const binding = data.scope.getOwnBinding(data.expression.name);
136
-
if (!binding) return [name, 0];
137
-
return [name, binding.references.length];
138
-
})
139
-
.sort(([, a], [, b]) => b! - a!)
140
-
.map(([name]) => name);
141
-
142
-
const dispatcher = usages[1].toString();
143
-
trigger(dispatcher, "FluxDispatcher");
144
-
return true;
145
-
}
146
-
});
147
-
148
-
register({
149
-
name: "FluxDispatcher",
150
-
manual: true,
151
-
process({ id, ast, lunast }) {
152
-
lunast.addModule({
153
-
name: "FluxDispatcher",
154
-
id,
155
-
type: "FluxDispatcher"
156
-
});
157
-
158
-
lunast.addType({
159
-
name: "FluxDispatcher",
160
-
fields: [
161
-
{
162
-
name: "default",
163
-
unmapped: "Z"
164
-
}
165
-
]
166
-
});
167
-
168
-
return true;
169
-
}
170
-
});
-17
packages/lunast/src/remap.ts
-17
packages/lunast/src/remap.ts
···
1
-
import type LunAST from ".";
2
-
import type { Program } from "estree-toolkit/dist/generated/types";
3
-
4
-
export type Processor = {
5
-
name: string;
6
-
find?: (string | RegExp)[] | (string | RegExp);
7
-
priority?: number;
8
-
manual?: boolean;
9
-
process: (state: ProcessorState) => boolean;
10
-
};
11
-
export type ProcessorState = {
12
-
id: string;
13
-
ast: Program;
14
-
lunast: LunAST;
15
-
markDirty: () => void;
16
-
trigger: (id: string, tag: string) => void;
17
-
};
-16
packages/lunast/src/types.ts
-16
packages/lunast/src/types.ts
···
1
-
export type RemapModule = {
2
-
name: string; // the name you require it by in your code
3
-
id: string; // the resolved webpack module ID (usually a number)
4
-
type: string;
5
-
};
6
-
7
-
export type RemapType = {
8
-
name: string;
9
-
fields: RemapField[];
10
-
};
11
-
12
-
export type RemapField = {
13
-
name: string; // the name of the field in the proxy (human readable)
14
-
unmapped?: string; // the name of the field in discord source (minified)
15
-
type?: string;
16
-
};
-211
packages/lunast/src/utils.ts
-211
packages/lunast/src/utils.ts
···
1
-
import type { Processor } from "./remap";
2
-
import { traverse, is, Scope, Binding, NodePath } from "estree-toolkit";
3
-
// FIXME something's fishy with these types
4
-
import type {
5
-
Expression,
6
-
ExpressionStatement,
7
-
ObjectExpression,
8
-
Program,
9
-
ReturnStatement
10
-
} from "estree-toolkit/dist/generated/types";
11
-
import { parse } from "meriyah";
12
-
13
-
export const processors: Processor[] = [];
14
-
15
-
export function register(processor: Processor) {
16
-
processors.push(processor);
17
-
}
18
-
19
-
export function getProcessors() {
20
-
// Clone the array to prevent mutation
21
-
return [...processors];
22
-
}
23
-
24
-
export type ExpressionWithScope = {
25
-
expression: Expression;
26
-
scope: Scope;
27
-
};
28
-
29
-
function getParent(path: NodePath) {
30
-
let parent = path.parentPath;
31
-
while (!is.program(parent)) {
32
-
parent = parent?.parentPath ?? null;
33
-
if (
34
-
parent == null ||
35
-
parent.node == null ||
36
-
![
37
-
"FunctionExpression",
38
-
"ExpressionStatement",
39
-
"CallExpression",
40
-
"Program"
41
-
].includes(parent.node.type)
42
-
) {
43
-
return null;
44
-
}
45
-
}
46
-
47
-
if (!is.functionExpression(path.parent)) return null;
48
-
return path.parent;
49
-
}
50
-
51
-
export function getExports(ast: Program) {
52
-
const ret: Record<string, ExpressionWithScope> = {};
53
-
54
-
traverse(ast, {
55
-
$: { scope: true },
56
-
BlockStatement(path) {
57
-
if (path.scope == null) return;
58
-
const parent = getParent(path);
59
-
if (parent == null) return;
60
-
61
-
for (let i = 0; i < parent.params.length; i++) {
62
-
const param = parent.params[i];
63
-
if (!is.identifier(param)) continue;
64
-
const binding: Binding | undefined = path.scope!.getBinding(param.name);
65
-
if (!binding) continue;
66
-
67
-
// module
68
-
if (i === 0) {
69
-
for (const reference of binding.references) {
70
-
if (!is.identifier(reference.node)) continue;
71
-
if (!is.assignmentExpression(reference.parentPath?.parentPath))
72
-
continue;
73
-
74
-
const exportsNode = reference.parentPath?.parentPath.node;
75
-
if (!is.memberExpression(exportsNode?.left)) continue;
76
-
if (!is.identifier(exportsNode.left.property)) continue;
77
-
if (exportsNode.left.property.name !== "exports") continue;
78
-
79
-
const exports = exportsNode?.right;
80
-
if (!is.objectExpression(exports)) continue;
81
-
82
-
for (const property of exports.properties) {
83
-
if (!is.property(property)) continue;
84
-
if (!is.identifier(property.key)) continue;
85
-
if (!is.expression(property.value)) continue;
86
-
ret[property.key.name] = {
87
-
expression: property.value,
88
-
scope: path.scope
89
-
};
90
-
}
91
-
}
92
-
}
93
-
// TODO: exports
94
-
else if (i === 1) {
95
-
for (const reference of binding.references) {
96
-
if (!is.identifier(reference.node)) continue;
97
-
if (reference.parentPath == null) continue;
98
-
if (!is.memberExpression(reference.parentPath.node)) continue;
99
-
if (!is.identifier(reference.parentPath.node.property)) continue;
100
-
101
-
const assignmentExpression = reference.parentPath.parentPath?.node;
102
-
if (!is.assignmentExpression(assignmentExpression)) continue;
103
-
104
-
ret[reference.parentPath.node.property.name] = {
105
-
expression: assignmentExpression.right,
106
-
scope: path.scope
107
-
};
108
-
}
109
-
}
110
-
}
111
-
}
112
-
});
113
-
114
-
return ret;
115
-
}
116
-
117
-
// TODO: util function to resolve the value of an expression
118
-
export function getPropertyGetters(ast: Program) {
119
-
const ret: Record<string, ExpressionWithScope> = {};
120
-
121
-
traverse(ast, {
122
-
$: { scope: true },
123
-
CallExpression(path) {
124
-
if (path.scope == null) return;
125
-
if (!is.callExpression(path.node)) return;
126
-
if (!is.memberExpression(path.node.callee)) return;
127
-
if (!is.identifier(path.node?.callee?.property)) return;
128
-
if (path.node.callee.property.name !== "d") return;
129
-
130
-
const arg = path.node.arguments.find((node): node is ObjectExpression =>
131
-
is.objectExpression(node)
132
-
);
133
-
if (!arg) return;
134
-
135
-
for (const property of arg.properties) {
136
-
if (!is.property(property)) continue;
137
-
if (!is.identifier(property.key)) continue;
138
-
if (!is.functionExpression(property.value)) continue;
139
-
if (!is.blockStatement(property.value.body)) continue;
140
-
141
-
const returnStatement = property.value.body.body.find(
142
-
(node): node is ReturnStatement => is.returnStatement(node)
143
-
);
144
-
if (!returnStatement || !returnStatement.argument) continue;
145
-
ret[property.key.name] = {
146
-
expression: returnStatement.argument,
147
-
scope: path.scope
148
-
};
149
-
}
150
-
151
-
this.stop();
152
-
}
153
-
});
154
-
155
-
return ret;
156
-
}
157
-
158
-
// The ESTree types are mismatched with estree-toolkit, but ESTree is a standard so this is fine
159
-
export function parseFixed(code: string): Program {
160
-
return parse(code) as any as Program;
161
-
}
162
-
163
-
export function magicAST(code: string) {
164
-
// Wraps code in an IIFE so you can type `return` and all that goodies
165
-
// Might not work for some other syntax issues but oh well
166
-
const tree = parse("(()=>{" + code + "})()");
167
-
168
-
const expressionStatement = tree.body[0] as ExpressionStatement;
169
-
if (!is.expressionStatement(expressionStatement)) return null;
170
-
if (!is.callExpression(expressionStatement.expression)) return null;
171
-
if (!is.arrowFunctionExpression(expressionStatement.expression.callee))
172
-
return null;
173
-
if (!is.blockStatement(expressionStatement.expression.callee.body))
174
-
return null;
175
-
return expressionStatement.expression.callee.body;
176
-
}
177
-
178
-
export function getImports(ast: Program) {
179
-
const ret: Record<string, ExpressionWithScope> = {};
180
-
181
-
traverse(ast, {
182
-
$: { scope: true },
183
-
BlockStatement(path) {
184
-
if (path.scope == null) return;
185
-
const parent = getParent(path);
186
-
if (parent == null) return;
187
-
188
-
const require = parent.params[2];
189
-
if (!is.identifier(require)) return;
190
-
const references = path.scope.getOwnBinding(require.name)?.references;
191
-
if (references == null) return;
192
-
for (const reference of references) {
193
-
if (!is.callExpression(reference.parentPath)) continue;
194
-
if (reference.parentPath.node?.arguments.length !== 1) continue;
195
-
if (!is.variableDeclarator(reference.parentPath.parentPath)) continue;
196
-
if (!is.identifier(reference.parentPath.parentPath.node?.id)) continue;
197
-
198
-
const moduleId = reference.parentPath.node.arguments[0];
199
-
if (!is.literal(moduleId)) continue;
200
-
if (moduleId.value == null) continue;
201
-
202
-
ret[moduleId.value.toString()] = {
203
-
expression: reference.parentPath.parentPath.node.id,
204
-
scope: path.scope
205
-
};
206
-
}
207
-
}
208
-
});
209
-
210
-
return ret;
211
-
}
+2
-1
packages/types/package.json
+2
-1
packages/types/package.json
···
9
9
"./*": "./src/*.ts"
10
10
},
11
11
"dependencies": {
12
-
"@moonlight-mod/lunast": "workspace:*",
12
+
"@moonlight-mod/lunast": "git+https://github.com/moonlight-mod/lunast.git",
13
+
"@moonlight-mod/moonmap": "git+https://github.com/moonlight-mod/moonmap.git",
13
14
"@types/flux": "^3.1.12",
14
15
"@types/react": "^18.2.22",
15
16
"csstype": "^3.1.2",
+2
packages/types/src/globals.ts
+2
packages/types/src/globals.ts
···
8
8
} from "./extension";
9
9
import type EventEmitter from "events";
10
10
import type LunAST from "@moonlight-mod/lunast";
11
+
import type Moonmap from "@moonlight-mod/moonmap";
11
12
12
13
export type MoonlightHost = {
13
14
asarPath: string;
···
46
47
getNatives: (ext: string) => any | undefined;
47
48
getLogger: (id: string) => Logger;
48
49
lunast: LunAST;
50
+
moonmap: Moonmap;
49
51
};
50
52
51
53
export enum MoonlightEnv {
+3
packages/types/src/index.ts
+3
packages/types/src/index.ts
···
19
19
export * from "./logger";
20
20
export * as constants from "./constants";
21
21
22
+
export type { AST } from "@moonlight-mod/lunast";
23
+
export { ModuleExport, ModuleExportType } from "@moonlight-mod/moonmap";
24
+
22
25
declare global {
23
26
const MOONLIGHT_ENV: MoonlightEnv;
24
27
const MOONLIGHT_PROD: boolean;
+3
-1
packages/web-preload/package.json
+3
-1
packages/web-preload/package.json
···
3
3
"private": true,
4
4
"dependencies": {
5
5
"@moonlight-mod/core": "workspace:*",
6
-
"@moonlight-mod/lunast": "workspace:*"
6
+
"@moonlight-mod/lunast": "git+https://github.com/moonlight-mod/lunast.git",
7
+
"@moonlight-mod/moonmap": "git+https://github.com/moonlight-mod/moonmap.git",
8
+
"@moonlight-mod/mappings": "git+https://github.com/moonlight-mod/mappings.git"
7
9
}
8
10
}
+5
-1
packages/web-preload/src/index.ts
+5
-1
packages/web-preload/src/index.ts
···
3
3
import { installStyles } from "@moonlight-mod/core/styles";
4
4
import Logger from "@moonlight-mod/core/util/logger";
5
5
import LunAST from "@moonlight-mod/lunast";
6
+
import Moonmap from "@moonlight-mod/moonmap";
7
+
import loadMappings from "@moonlight-mod/mappings";
6
8
7
9
(async () => {
8
10
const logger = new Logger("web-preload");
···
18
20
getLogger(id) {
19
21
return new Logger(id);
20
22
},
21
-
lunast: new LunAST()
23
+
lunast: new LunAST(),
24
+
moonmap: new Moonmap()
22
25
};
23
26
24
27
try {
28
+
loadMappings(window.moonlight.moonmap, window.moonlight.lunast);
25
29
await loadProcessedExtensions(moonlightNode.processedExtensions);
26
30
await installWebpackPatcher();
27
31
} catch (e) {
+46
-18
pnpm-lock.yaml
+46
-18
pnpm-lock.yaml
···
45
45
packages/core:
46
46
dependencies:
47
47
'@moonlight-mod/lunast':
48
-
specifier: workspace:*
49
-
version: link:../lunast
48
+
specifier: git+https://github.com/moonlight-mod/lunast.git
49
+
version: https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a
50
+
'@moonlight-mod/moonmap':
51
+
specifier: git+https://github.com/moonlight-mod/moonmap.git
52
+
version: https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319
50
53
'@moonlight-mod/types':
51
54
specifier: workspace:*
52
55
version: link:../types
···
69
72
specifier: workspace:*
70
73
version: link:../types
71
74
72
-
packages/lunast:
73
-
dependencies:
74
-
astring:
75
-
specifier: ^1.9.0
76
-
version: 1.9.0
77
-
estree-toolkit:
78
-
specifier: ^1.7.8
79
-
version: 1.7.8
80
-
meriyah:
81
-
specifier: ^6.0.1
82
-
version: 6.0.1
83
-
84
75
packages/node-preload:
85
76
dependencies:
86
77
'@moonlight-mod/core':
···
93
84
packages/types:
94
85
dependencies:
95
86
'@moonlight-mod/lunast':
96
-
specifier: workspace:*
97
-
version: link:../lunast
87
+
specifier: git+https://github.com/moonlight-mod/lunast.git
88
+
version: https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a
89
+
'@moonlight-mod/moonmap':
90
+
specifier: git+https://github.com/moonlight-mod/moonmap.git
91
+
version: https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319
98
92
'@types/flux':
99
93
specifier: ^3.1.12
100
94
version: 3.1.12
···
114
108
specifier: workspace:*
115
109
version: link:../core
116
110
'@moonlight-mod/lunast':
117
-
specifier: workspace:*
118
-
version: link:../lunast
111
+
specifier: git+https://github.com/moonlight-mod/lunast.git
112
+
version: https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a
113
+
'@moonlight-mod/mappings':
114
+
specifier: git+https://github.com/moonlight-mod/mappings.git
115
+
version: https://codeload.github.com/moonlight-mod/mappings/tar.gz/8512c9df931a4a62a03e23c64a7d378602806128(@moonlight-mod/lunast@https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a)(@moonlight-mod/moonmap@https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319)
116
+
'@moonlight-mod/moonmap':
117
+
specifier: git+https://github.com/moonlight-mod/moonmap.git
118
+
version: https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319
119
119
120
120
packages:
121
121
···
290
290
'@humanwhocodes/object-schema@2.0.1':
291
291
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
292
292
deprecated: Use @eslint/object-schema instead
293
+
294
+
'@moonlight-mod/lunast@https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a':
295
+
resolution: {tarball: https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a}
296
+
version: 1.0.0
297
+
298
+
'@moonlight-mod/mappings@https://codeload.github.com/moonlight-mod/mappings/tar.gz/8512c9df931a4a62a03e23c64a7d378602806128':
299
+
resolution: {tarball: https://codeload.github.com/moonlight-mod/mappings/tar.gz/8512c9df931a4a62a03e23c64a7d378602806128}
300
+
version: 1.0.0
301
+
peerDependencies:
302
+
'@moonlight-mod/lunast': git+https://github.com/moonlight-mod/lunast.git
303
+
'@moonlight-mod/moonmap': git+https://github.com/moonlight-mod/moonmap.git
304
+
305
+
'@moonlight-mod/moonmap@https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319':
306
+
resolution: {tarball: https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319}
307
+
version: 1.0.0
293
308
294
309
'@nodelib/fs.scandir@2.1.5':
295
310
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
···
1454
1469
'@humanwhocodes/module-importer@1.0.1': {}
1455
1470
1456
1471
'@humanwhocodes/object-schema@2.0.1': {}
1472
+
1473
+
'@moonlight-mod/lunast@https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a':
1474
+
dependencies:
1475
+
astring: 1.9.0
1476
+
estree-toolkit: 1.7.8
1477
+
meriyah: 6.0.1
1478
+
1479
+
'@moonlight-mod/mappings@https://codeload.github.com/moonlight-mod/mappings/tar.gz/8512c9df931a4a62a03e23c64a7d378602806128(@moonlight-mod/lunast@https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a)(@moonlight-mod/moonmap@https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319)':
1480
+
dependencies:
1481
+
'@moonlight-mod/lunast': https://codeload.github.com/moonlight-mod/lunast/tar.gz/af98b963bf8b6d00301229b094811a55f96eca0a
1482
+
'@moonlight-mod/moonmap': https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319
1483
+
1484
+
'@moonlight-mod/moonmap@https://codeload.github.com/moonlight-mod/moonmap/tar.gz/79cfb0f84f62c910ff6eb3cf314e045110b9d319': {}
1457
1485
1458
1486
'@nodelib/fs.scandir@2.1.5':
1459
1487
dependencies: