this repo has no description

LunAST: Trigger processors from other processors

+196 -66
+25 -11
packages/core/src/patch.ts
··· 74 } 75 } 76 77 for (const [id, func] of Object.entries(entry)) { 78 if (func.__moonlight === true) continue; 79 - let moduleString = Object.hasOwn(moduleCache, id) 80 - ? moduleCache[id] 81 - : func.toString().replace(/\n/g, ""); 82 83 for (let i = 0; i < patches.length; i++) { 84 const patch = patches[i]; ··· 151 } 152 } 153 154 try { 155 - let parsed = moonlight.lunast.parseScript(id, `(\n${moduleString}\n)`); 156 if (parsed != null) { 157 - // parseScript adds an extra ; for some reason 158 - parsed = parsed.trimEnd().substring(0, parsed.lastIndexOf(";")); 159 - if (patchModule(id, "lunast", parsed)) { 160 - moduleString = parsed; 161 } 162 } 163 } catch (e) { ··· 170 !entry[id].__moonlight 171 ) { 172 const wrapped = 173 - `(${moduleString}).apply(this, arguments)\n` + 174 `//# sourceURL=Webpack-Module-${id}`; 175 entry[id] = new Function( 176 "module", ··· 181 entry[id].__moonlight = true; 182 } 183 } 184 - 185 - moduleCache[id] = moduleString; 186 } 187 } 188 ··· 302 */ 303 export async function installWebpackPatcher() { 304 await handleModuleDependencies(); 305 306 let realWebpackJsonp: WebpackJsonp | null = null; 307 Object.defineProperty(window, "webpackChunkdiscord_app", {
··· 74 } 75 } 76 77 + // Populate the module cache 78 + for (const [id, func] of Object.entries(entry)) { 79 + if (!Object.hasOwn(moduleCache, id) && func.__moonlight !== true) { 80 + moduleCache[id] = func.toString().replace(/\n/g, ""); 81 + } 82 + } 83 + 84 for (const [id, func] of Object.entries(entry)) { 85 if (func.__moonlight === true) continue; 86 + let moduleString = moduleCache[id]; 87 88 for (let i = 0; i < patches.length; i++) { 89 const patch = patches[i]; ··· 156 } 157 } 158 159 + moduleCache[id] = moduleString; 160 + 161 try { 162 + const parsed = moonlight.lunast.parseScript(id, moduleString); 163 if (parsed != null) { 164 + 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; 172 + } 173 } 174 } 175 } catch (e) { ··· 182 !entry[id].__moonlight 183 ) { 184 const wrapped = 185 + `(${moduleCache[id]}).apply(this, arguments)\n` + 186 `//# sourceURL=Webpack-Module-${id}`; 187 entry[id] = new Function( 188 "module", ··· 193 entry[id].__moonlight = true; 194 } 195 } 196 } 197 } 198 ··· 312 */ 313 export async function installWebpackPatcher() { 314 await handleModuleDependencies(); 315 + 316 + moonlight.lunast.setModuleSourceGetter((id) => { 317 + return moduleCache[id] ?? null; 318 + }); 319 320 let realWebpackJsonp: WebpackJsonp | null = null; 321 Object.defineProperty(window, "webpackChunkdiscord_app", {
+1 -1
packages/lunast/README.md
··· 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
··· 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
+49 -20
packages/lunast/src/index.ts
··· 16 >; 17 private processors: Processor[]; 18 private defaultRequire?: (id: string) => any; 19 20 elapsed: number; 21 ··· 37 return "dev"; 38 } 39 40 - public parseScript(id: string, code: string): string | null { 41 const start = performance.now(); 42 43 const available = [...this.processors] 44 .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)) 45 - .filter((x) => 46 - x.find != null 47 - ? typeof x.find === "string" 48 - ? code.indexOf(x.find) !== -1 49 - : x.find.test(code) 50 - : true 51 - ) 52 - .filter((x) => 53 - x.dependencies != null 54 - ? x.dependencies.every((dep) => this.successful.has(dep)) 55 - : true 56 - ); 57 - if (available.length === 0) return null; 58 59 - const module = parseFixed(code); 60 let dirty = false; 61 const state: ProcessorState = { 62 id, ··· 64 lunast: this, 65 markDirty: () => { 66 dirty = true; 67 } 68 }; 69 70 - for (const processor of available) { 71 if (processor.process(state)) { 72 this.processors.splice(this.processors.indexOf(processor), 1); 73 this.successful.add(processor.name); ··· 75 } 76 77 const str = dirty ? generate(module) : null; 78 - 79 - const end = performance.now(); 80 - this.elapsed += end - start; 81 82 - return str; 83 } 84 85 public getType(name: string) { ··· 140 // TODO: call this with require we obtain from the webpack entrypoint 141 public setDefaultRequire(require: (id: string) => any) { 142 this.defaultRequire = require; 143 } 144 145 public remap<Id extends keyof Remapped>(
··· 16 >; 17 private processors: Processor[]; 18 private defaultRequire?: (id: string) => any; 19 + private getModuleSource?: (id: string) => string; 20 21 elapsed: number; 22 ··· 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, ··· 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); ··· 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) { ··· 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>(
+52 -5
packages/lunast/src/modules/test.ts
··· 1 import { traverse, is } from "estree-toolkit"; 2 - import { getPropertyGetters, register, magicAST } 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 ··· 44 "balls" 45 )`)!; 46 for (const data of Object.values(getters)) { 47 - if (!is.identifier(data.argument)) continue; 48 49 - const node = data.scope.getOwnBinding(data.argument.name); 50 if (!node) continue; 51 52 const body = node.path.get<BlockStatement>("body"); ··· 67 const fields = []; 68 69 for (const [name, data] of Object.entries(getters)) { 70 - if (!is.identifier(data.argument)) continue; 71 - const node = data.scope.getOwnBinding(data.argument.name); 72 if (!node) continue; 73 74 let isSupportsCopy = false; ··· 121 return false; 122 } 123 });*/
··· 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 ··· 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"); ··· 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; ··· 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 + });
+3 -2
packages/lunast/src/remap.ts
··· 3 4 export type Processor = { 5 name: string; 6 - find?: string | RegExp; // TODO: allow multiple finds 7 priority?: number; 8 - dependencies?: string[]; // FIXME: this can skip modules 9 process: (state: ProcessorState) => boolean; 10 }; 11 export type ProcessorState = { ··· 13 ast: Program; 14 lunast: LunAST; 15 markDirty: () => void; 16 };
··· 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 = { ··· 13 ast: Program; 14 lunast: LunAST; 15 markDirty: () => void; 16 + trigger: (id: string, tag: string) => void; 17 };
+66 -27
packages/lunast/src/utils.ts
··· 1 import type { Processor } from "./remap"; 2 - import { traverse, is, Scope, Binding } from "estree-toolkit"; 3 // FIXME something's fishy with these types 4 import type { 5 Expression, ··· 22 } 23 24 export type ExpressionWithScope = { 25 - argument: Expression; 26 scope: Scope; 27 }; 28 29 export function getExports(ast: Program) { 30 const ret: Record<string, ExpressionWithScope> = {}; 31 ··· 33 $: { scope: true }, 34 BlockStatement(path) { 35 if (path.scope == null) return; 36 37 - // Walk up to make sure we are indeed the top level 38 - let parent = path.parentPath; 39 - while (!is.program(parent)) { 40 - parent = parent?.parentPath ?? null; 41 - if ( 42 - parent == null || 43 - parent.node == null || 44 - ![ 45 - "FunctionExpression", 46 - "ExpressionStatement", 47 - "CallExpression", 48 - "Program" 49 - ].includes(parent.node.type) 50 - ) { 51 - return; 52 - } 53 - } 54 - 55 - if (!is.functionExpression(path.parent)) return; 56 - 57 - for (let i = 0; i < path.parent.params.length; i++) { 58 - const param = path.parent.params[i]; 59 if (!is.identifier(param)) continue; 60 const binding: Binding | undefined = path.scope!.getBinding(param.name); 61 if (!binding) continue; ··· 80 if (!is.identifier(property.key)) continue; 81 if (!is.expression(property.value)) continue; 82 ret[property.key.name] = { 83 - argument: property.value, 84 scope: path.scope 85 }; 86 } ··· 98 if (!is.assignmentExpression(assignmentExpression)) continue; 99 100 ret[reference.parentPath.node.property.name] = { 101 - argument: assignmentExpression.right, 102 scope: path.scope 103 }; 104 } ··· 139 ); 140 if (!returnStatement || !returnStatement.argument) continue; 141 ret[property.key.name] = { 142 - argument: returnStatement.argument, 143 scope: path.scope 144 }; 145 } ··· 170 return null; 171 return expressionStatement.expression.callee.body; 172 }
··· 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, ··· 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 ··· 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; ··· 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 } ··· 102 if (!is.assignmentExpression(assignmentExpression)) continue; 103 104 ret[reference.parentPath.node.property.name] = { 105 + expression: assignmentExpression.right, 106 scope: path.scope 107 }; 108 } ··· 143 ); 144 if (!returnStatement || !returnStatement.argument) continue; 145 ret[property.key.name] = { 146 + expression: returnStatement.argument, 147 scope: path.scope 148 }; 149 } ··· 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 + }