this repo has no description

LunAST: Trigger processors from other processors

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