diff --git a/common/vendor-libs/patch-steps-lib.d.ts b/common/vendor-libs/patch-steps-lib.d.ts deleted file mode 100644 index 5519c27..0000000 --- a/common/vendor-libs/patch-steps-lib.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'ultimate-crosscode-typedefs/patch-steps-lib'; diff --git a/common/vendor-libs/patch-steps-lib.js b/common/vendor-libs/patch-steps-lib.ts similarity index 100% rename from common/vendor-libs/patch-steps-lib.js rename to common/vendor-libs/patch-steps-lib.ts diff --git a/common/vendor-libs/patch-steps-lib/src/patchsteps-callable.js b/common/vendor-libs/patch-steps-lib/src/patchsteps-callable.ts similarity index 60% rename from common/vendor-libs/patch-steps-lib/src/patchsteps-callable.js rename to common/vendor-libs/patch-steps-lib/src/patchsteps-callable.ts index 32747f0..9385906 100644 --- a/common/vendor-libs/patch-steps-lib/src/patchsteps-callable.js +++ b/common/vendor-libs/patch-steps-lib/src/patchsteps-callable.ts @@ -1,32 +1,21 @@ import {appliers, DebugState} from "./patchsteps-patch.js"; -/** - * @typedef State - * @property {unknown} currentValue - * @property {unknown[]} stack - * @property {(fromGame: boolean| string, path: string) => Promise} - * @property {DebugState} debugState - * @property {boolean} debug - * / +export interface State { + currentValue: unknown, + stack: unknown[], + debugState: DebugState, + debug: boolean +} /** * A user defined step that is distinguishable from builtin PatchSteps. * Errors that occur in callables are not handled by the PatchSteps interpreter. - * - * @async - * @callback Callable - * @param {State} state is the internal PatchStep state. - * @param {unknown} args is the user supplied arguments. */ +export type Callable = (state: State, args: unknown) => Promise; -/* @type {Map} */ -const callables = new Map; +const callables = new Map(); -/** - * @param {string} id - * @param {Callable} callable - */ -export function register(id, callable) { +export function register(id: string, callable: Callable) { if (typeof id !== "string") { throw Error('Id must be a string'); } @@ -44,7 +33,7 @@ export function register(id, callable) { callables.set(id, callable); } -appliers["CALL"] = async function(state) { +appliers["CALL"] = async function(state: State) { const id = this["id"]; const args = this["args"]; @@ -61,12 +50,12 @@ appliers["CALL"] = async function(state) { const callable = callables.get(id); try { - await callable(state, args); + await callable!(state, args); } catch (e) { if (e !== state.debugState) { // So they know what happened console.error(e); - state.debugState.throwError('ValueError', `Callable ${i} did not properly throw an error.`); + state.debugState.throwError('ValueError', `Callable ${id} did not properly throw an error.`); } // They properly threw the error throw e; diff --git a/common/vendor-libs/patch-steps-lib/src/patchsteps-diff.js b/common/vendor-libs/patch-steps-lib/src/patchsteps-diff.ts similarity index 72% rename from common/vendor-libs/patch-steps-lib/src/patchsteps-diff.js rename to common/vendor-libs/patch-steps-lib/src/patchsteps-diff.ts index ccd7a1d..de372b4 100644 --- a/common/vendor-libs/patch-steps-lib/src/patchsteps-diff.js +++ b/common/vendor-libs/patch-steps-lib/src/patchsteps-diff.ts @@ -13,23 +13,24 @@ */ import {photocopy, photomerge} from "./patchsteps-utils.js"; +import {AnyPatchStep, BasePatchStep, DiffSettings, Index, PatchFile, PatchStep, unsafeAssert} from './types.js' /** * A difference heuristic. - * @param {any} a The first value to check. - * @param {any} b The second value to check. - * @param {any} settings The involved control settings. - * @returns {number} A difference value from 0 (same) to 1 (different). + * @param a The first value to check. + * @param b The second value to check. + * @param settings The involved control settings. + * @returns A difference value from 0 (same) to 1 (different). */ -function diffHeuristic(a, b, settings) { - if ((a === null) && (b === null)) +function diffHeuristic(a: unknown, b: unknown, settings: DiffSettings): number { + if ((a === null) && (b === null) || (a === undefined) && (b === undefined)) return 0; - if ((a === null) || (b === null)) - return null; + if ((a === null) || (b === null) || (a === undefined) || (b === undefined)) + return 1; if (a.constructor !== b.constructor) return 1; - if (a.constructor === Array) { + if (Array.isArray(a) && Array.isArray(b)) { let array = diffArrayHeuristic(a, b, settings); if (array.length == 0) return 0; @@ -50,8 +51,10 @@ function diffHeuristic(a, b, settings) { } } return changes / array.length; - } else if (a.constructor === Object) { - let total = []; + } else if (a.constructor === Object && b.constructor === Object) { + unsafeAssert>(a); + unsafeAssert>(b); + let total: string[] = []; for (let k in a) total.push(k); for (let k in b) @@ -64,7 +67,7 @@ function diffHeuristic(a, b, settings) { } else if ((total[i] in b) && !(total[i] in a)) { change += settings.diffAddDelKey; } else { - change += diffHeuristic(a[total[i]], b[total[i]], settings) * settings.diffMulSameKey; + change += diffHeuristic((a as Record)[total[i]], (b as Record)[total[i]], settings) * settings.diffMulSameKey; } } if (total.length != 0) @@ -91,13 +94,13 @@ function diffHeuristic(a, b, settings) { * The actual implementation is different to this description, but follows the same rules. * Stack A and the output are the same. */ -function diffArrayHeuristic(a, b, settings) { +function diffArrayHeuristic(a: unknown[], b: unknown[], settings: DiffSettings) { const lookahead = settings.arrayLookahead; - let sublog = []; + let sublog: string[] = []; let ia = 0; for (let i = 0; i < b.length; i++) { let validDif = 2; - let validSrc = null; + let validSrc: number | null = null; for (let j = ia; j < Math.min(ia + lookahead, a.length); j++) { let dif = diffHeuristic(a[j], b[i], settings); if (dif < validDif) { @@ -132,13 +135,13 @@ function diffArrayHeuristic(a, b, settings) { /** * Diffs two objects. This is actually an outer wrapper, which provides default settings along with optimization. - * - * @param {any} a The original value - * @param {any} b The target value - * @param {object} [settings] Optional bunch of settings. May include "comment". - * @return {object[]|null} Null if unpatchable (this'll never occur for two Objects or two Arrays), Array of JSON-ready Patch Steps otherwise + * + * @param a The original value + * @param b The target value + * @param settings Optional bunch of settings. May include "comment". + * @return Null if unpatchable (this'll never occur for two Objects or two Arrays), Array of JSON-ready Patch Steps otherwise */ -export function diff(a, b, settings) { +export function diff(a: unknown, b: unknown, settings: Partial) { let trueSettings = photocopy(defaultSettings); if (settings !== void 0) photomerge(trueSettings, settings); @@ -146,10 +149,11 @@ export function diff(a, b, settings) { trueSettings.commentValue = trueSettings.comment; let result = trueSettings.diffCore(a, b, trueSettings); + if (!result) return null; if (trueSettings.optimize) { for (let i = 1; i < result.length; i++) { let here = result[i]; - let prev = result[i - 1]; + let prev: AnyPatchStep = result[i - 1]; let optimizedOut = false; if (here["type"] == "EXIT") { if (prev["type"] == "EXIT") { @@ -158,7 +162,8 @@ export function diff(a, b, settings) { here["count"] = 1; if (!("count" in prev)) prev["count"] = 1; - prev["count"] += here["count"]; + unsafeAssert(prev); + prev["count"] += here["count"]!; // Copy comments backwards to try and preserve the unoptimized autocommenter semantics if ("comment" in here) prev["comment"] = here["comment"]; @@ -167,9 +172,9 @@ export function diff(a, b, settings) { } else if (here["type"] == "ENTER") { if (prev["type"] == "ENTER") { // Crush ENTERs - if (prev["index"].constructor !== Array) + if (!Array.isArray(prev["index"])) prev["index"] = [prev["index"]]; - if (here["index"].constructor !== Array) + if (!Array.isArray(here["index"])) here["index"] = [here["index"]]; prev["index"] = prev["index"].concat(here["index"]); optimizedOut = true; @@ -186,10 +191,10 @@ export function diff(a, b, settings) { /** * Adds a comment to the step if there is a comment in settings.commentValue. - * @param {object} step The step to add to. - * @param {object} settings The settings. + * @param step The step to add to. + * @param settings The settings. */ -export function diffApplyComment(step, settings) { +export function diffApplyComment(step: T, settings: DiffSettings) { if (settings.commentValue !== void 0) step.comment = settings.commentValue; return step; @@ -197,13 +202,13 @@ export function diffApplyComment(step, settings) { /** * Handles the bookkeeping in settings necessary when entering a level of the diff. - * @param {any} a The original value - * @param {any} b The target value - * @param {string | number} index The index. - * @param {object} settings Settings. - * @return {object[]|null} See diff for more details + * @param a The original value + * @param b The target value + * @param index The index. + * @param settings Settings. + * @return See diff for more details */ -export function diffEnterLevel(a, b, index, settings) { +export function diffEnterLevel(a: unknown, b: unknown, index: Index, settings: DiffSettings): AnyPatchStep[] | null { settings.path.push(index); if (settings.comment !== void 0) settings.commentValue = settings.comment + "." + settings.path.join("."); @@ -213,16 +218,16 @@ export function diffEnterLevel(a, b, index, settings) { } // This is the default diffCore. -function diffInterior(a, b, settings) { - if ((a === null) && (b === null)) +function diffInterior(a: unknown, b: unknown, settings: DiffSettings) { + if ((a === null) && (b === null) || (a === undefined) && (b === undefined)) return []; - if ((a === null) || (b === null)) + if ((a === null) || (b === null) || (a === undefined) || (b === undefined)) return null; if (a.constructor !== b.constructor) return null; - let log = []; + let log: AnyPatchStep[] = []; - if (a.constructor === Array) { + if (Array.isArray(a) && Array.isArray(b)) { let array = diffArrayHeuristic(a, b, settings); let ai = 0; let bi = 0; @@ -232,10 +237,10 @@ function diffInterior(a, b, settings) { // At patch time, a[ai + x] for arbitrary 'x' is in the live array at [bi + x] for (let i = 0; i < array.length; i++) { if (array[i] == "POPA") { - log.push(diffApplyComment({"type": "REMOVE_ARRAY_ELEMENT", "index": bi}, settings)); + log.push(diffApplyComment({"type": "REMOVE_ARRAY_ELEMENT", "index": bi} as PatchStep.REMOVE_ARRAY_ELEMENT, settings)); ai++; } else if (array[i] == "INSERT") { - let insertion = diffApplyComment({"type": "ADD_ARRAY_ELEMENT", "index": bi, "content": photocopy(b[bi])}, settings); + let insertion = diffApplyComment({"type": "ADD_ARRAY_ELEMENT", "index": bi, "content": photocopy(b[bi])} as PatchStep.ADD_ARRAY_ELEMENT, settings); // Is this a set of elements being inserted at the end? let j; for (j = i + 1; j < array.length; j++) @@ -255,17 +260,22 @@ function diffInterior(a, b, settings) { log.push({"type": "EXIT"}); } } else { - log.push(diffApplyComment({"type": "SET_KEY", "index": bi, "content": photocopy(b[bi])}, settings)); + log.push(diffApplyComment({"type": "SET_KEY", "index": bi, "content": photocopy(b[bi])} as PatchStep.SET_KEY, settings)); } ai++; bi++; } } - } else if (a.constructor === Object) { + } else if (a.constructor === Object && b.constructor === Object) { + unsafeAssert>(a); + unsafeAssert>(b); for (let k in a) { if (k in b) { + unsafeAssert>(a); + unsafeAssert>(b); + if (diffHeuristic(a[k], b[k], settings) >= settings.trulyDifferentThreshold) { - log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy(b[k])}, settings)); + log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy(b[k])} as PatchStep.SET_KEY, settings)); } else { let xd = diffEnterLevel(a[k], b[k], k, settings); if (xd != null) { @@ -276,16 +286,16 @@ function diffInterior(a, b, settings) { } } else { // should it happen? probably not. will it happen? maybe - log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy(b[k])}, settings)); + log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy(b[k])} as PatchStep.SET_KEY, settings)); } } } else { - log.push(diffApplyComment({"type": "SET_KEY", "index": k}, settings)); + log.push(diffApplyComment({"type": "SET_KEY", "index": k} as PatchStep.SET_KEY, settings)); } } for (let k in b) if (!(k in a)) - log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy(b[k])}, settings)); + log.push(diffApplyComment({"type": "SET_KEY", "index": k, "content": photocopy((b as Record)[k])} as PatchStep.SET_KEY, settings)); } else if (a != b) { return null; } diff --git a/common/vendor-libs/patch-steps-lib/src/patchsteps-patch.js b/common/vendor-libs/patch-steps-lib/src/patchsteps-patch.ts similarity index 73% rename from common/vendor-libs/patch-steps-lib/src/patchsteps-patch.js rename to common/vendor-libs/patch-steps-lib/src/patchsteps-patch.ts index 204715a..accc51c 100644 --- a/common/vendor-libs/patch-steps-lib/src/patchsteps-patch.js +++ b/common/vendor-libs/patch-steps-lib/src/patchsteps-patch.ts @@ -13,38 +13,28 @@ */ import {photocopy, photomerge} from "./patchsteps-utils.js"; - -// The following are definitions used for reference in DebugState. -/* - * ParsedPath is actually any type that translateParsedPath can understand. - * And translateParsedPath can be overridden by the user. - * But the types declared here are those that will be received no matter what. - * declare type ParsedPath = null | [fromGame: true | false | string, url: string]; - * - * declare type FileInfo = { - * path: string; - * stack: StackEntry[]; - * }; - * - * declare type StackEntry = StackEntryStep | StackEntryError; - * declare type StackEntryStep = { - * type: "Step"; - * }; - * declare type StackEntryError = { - * type: "Error"; - * errorType: string; - * errorMessage: string; - * }; - */ +import { + AnyPatchStep, + Applier, + Appliers, + ApplierState, + FileInfo, + Index, + Loader, + ParsedPath, + PatchFile, PatchStep, StackEntry, + StackEntryStep, unsafeAssert +} from './types.js' // Error handling for appliers. // You are expected to subclass this class if you want additional functionality. export class DebugState { + fileStack: FileInfo[]; + currentFile: FileInfo | null; + // The constructor. The default state of a DebugState is invalid; a file must be added (even if null) to make it valid. constructor() { - // FileInfo[] this.fileStack = []; - // FileInfo this.currentFile = null; } @@ -52,7 +42,7 @@ export class DebugState { * Translates a ParsedPath into a string. * Overridable. */ - translateParsedPath(parsedPath) { + translateParsedPath(parsedPath: ParsedPath | null): string { if (parsedPath === null) return "(unknown file)"; // By default, we know nothing. @@ -70,7 +60,7 @@ export class DebugState { * Enters a file by parsedPath. Do not override. * @final */ - addFile(parsedPath) { + addFile(parsedPath: ParsedPath): void { const path = this.translateParsedPath(parsedPath); const fileInfo = { path, @@ -84,18 +74,18 @@ export class DebugState { * Removes a pushed file. * @final */ - removeLastFile() { + removeLastFile(): FileInfo { const lastFile = this.fileStack.pop(); this.currentFile = this.fileStack[this.fileStack.length - 1]; - return lastFile; + return lastFile!; } - + /** * Enters a step. Note that calls to this *surround* applyStep as the index is not available to it. * @final */ - addStep(index, name = "") { - this.currentFile.stack.push({ + addStep(index: Index, name = ""): void { + this.currentFile!.stack.push({ type: "Step", index, name @@ -106,41 +96,43 @@ export class DebugState { * Leaves a step. * @final */ - removeLastStep() { - const stack = this.currentFile.stack; - let currentStep = null; + removeLastStep(): StackEntryStep { + const stack = this.currentFile!.stack; + let currentStep: StackEntryStep | null = null; for(let index = stack.length - 1; index >= 0; index--) { - if (stack[index].type === "Step") { - currentStep = stack[index]; + const entry = stack[index]; + if (entry.type === "Step") { + currentStep = entry; stack.splice(index,1); index = -1; } } - return currentStep; + return currentStep!; } - + /** * Gets the last (i.e. current) step. * @final */ - getLastStep() { - const stack = this.currentFile.stack; - let currentStep = null; + getLastStep(): StackEntryStep { + const stack = this.currentFile!.stack; + let currentStep: StackEntryStep | null = null; for(let index = stack.length - 1; index >= 0; index--) { - if (stack[index].type === "Step") { - currentStep = stack[index]; + const entry = stack[index]; + if (entry.type === "Step") { + currentStep = entry; index = -1; } } - return currentStep; + return currentStep!; } - + /** * Throws this instance as an error. * @final */ - throwError(type, message) { - this.currentFile.stack.push({ + throwError(type: string, message: string): void { + this.currentFile!.stack.push({ type: "Error", errorType: type, errorMessage: message @@ -152,7 +144,7 @@ export class DebugState { * Prints information about a specific file on the stack. * Overridable. */ - printFileInfo(file) { + printFileInfo(file: FileInfo): void { console.log(`File %c${file.path}`, 'red'); let message = ''; const stack = file.stack; @@ -175,12 +167,12 @@ export class DebugState { } console.log(message); } - + /** * Prints information about the whole stack. * @final */ - print() { + print(): void { for(let fileIndex = 0; fileIndex < this.fileStack.length; fileIndex++) { this.printFileInfo(this.fileStack[fileIndex]); } @@ -190,44 +182,47 @@ export class DebugState { * Run at the start of applyStep; after the step has been entered formally, but before executing it. * Overridable. */ - async beforeStep() { - + async beforeStep(): Promise { + } /** * Run at the end of applyStep; after executing the step, but before leaving it formally. * Overridable. */ - async afterStep() { - + async afterStep(): Promise { + } } // Custom extensions are registered here. // Their 'this' is the Step, they are passed the state, and they are expected to return a Promise. // In practice this is done with async old-style functions. -export const appliers = {}; +export const appliers = {} as Appliers; /* - * @param {any} a The object to modify - * @param {object|object[]} steps The patch, fresh from the JSON. Can be in legacy or Patch Steps format. - * @param {(fromGame: boolean | string, path: string) => Promise} loader The loading function. + * @param a The object to modify + * @param steps The patch, fresh from the JSON. Can be in legacy or Patch Steps format. + * @param loader The loading function. * NOTE! IF CHANGING THIS, KEEP IN MIND DEBUGSTATE translatePath GETS ARGUMENTS ARRAY OF THIS. * ALSO KEEP IN MIND THE parsePath FUNCTION! * For fromGame: false this gets a file straight from the mod, such as "package.json". * For fromGame: true this gets a file from the game, which is patched by the host if relevant. * If the PatchSteps file passes a protocol that is not understood, then, and only then, will a string be passed (without the ":" at the end) * In this case, fromGame is set to that string, instead. - * @param [debugState] debugState The DebugState stack tracer. + * @param debugState The DebugState stack tracer. * If not given, will be created. You need to pass your own instance of this to have proper filename tracking. - * @return {Promise} A Promise + * @return A Promise */ -export async function patch(a, steps, loader, debugState) { +export async function patch(a: unknown, steps: PatchFile, loader: Loader, debugState?: DebugState) { if (!debugState) { debugState = new DebugState(); debugState.addFile(null); } if (steps.constructor === Object) { + unsafeAssert>>(steps); + unsafeAssert>(a); + // Standardized Mods specification for (let k in steps) { // Switched back to a literal translation in 1.0.2 to make it make sense with spec, it's more awkward but simpler. @@ -243,6 +238,7 @@ export async function patch(a, steps, loader, debugState) { } return; } + unsafeAssert(steps); const state = { currentValue: a, stack: [], @@ -254,7 +250,7 @@ export async function patch(a, steps, loader, debugState) { for (let index = 0; index < steps.length; index++) { try { debugState.addStep(index); - await applyStep(steps[index], state, debugState); + await applyStep(steps[index], state); debugState.removeLastStep(); } catch(e) { debugState.print(); @@ -266,55 +262,55 @@ export async function patch(a, steps, loader, debugState) { } } -async function applyStep(step, state) { +async function applyStep(step: AnyPatchStep, state: ApplierState) { await state.debugState.beforeStep(); state.debugState.getLastStep().name = step["type"]; if (!appliers[step["type"]]) { state.debugState.getLastStep().name = ''; state.debugState.throwError('TypeError',`${step['type']} is not a valid type.`); } - await appliers[step["type"]].call(step, state); + await (appliers[step["type"]] as Applier).call(step, state); await state.debugState.afterStep(); } -function replaceObjectProperty(object, key, keyword, value) { - let oldValue = object[key]; +function replaceObjectProperty(object: O, key: keyof O, keyword: string | Record, value: string | {[replacementId: string]: string | number}) { + let oldValue = object[key] as string; // It's more complex than we thought. if (!Array.isArray(keyword) && typeof keyword === "object") { // go through each and check if it matches anywhere. for(const property in keyword) { if (keyword[property]) { - object[key] = oldValue.replace(new RegExp(keyword[property], "g"), value[property] || ""); - oldValue = object[key]; + object[key] = oldValue.replace(new RegExp(keyword[property], "g"), (value as {[replacementId: string]: string | number})[property] as string || "") as O[keyof O]; + oldValue = object[key] as string; } } } else { - object[key] = oldValue.replace(new RegExp(keyword, "g"), value); + object[key] = oldValue.replace(new RegExp(keyword as string, "g"), value as string) as O[keyof O]; } } /** - * @param {object} obj The object to search and replace the values of - * @param {RegExp| {[replacementId: string]: RegExp}} keyword The expression to match against - * @param {String| {[replacementId]: string | number}} value The value the replace the match - * @returns {void} + * @param obj The object to search and replace the values of + * @param keyword The expression to match against + * @param value The value the replace the match * */ -function valueInsertion(obj, keyword, value) { +function valueInsertion(obj: unknown, keyword: string | Record, value: string | {[replacementId: string]: string | number}) { if (Array.isArray(obj)) { for (let index = 0; index < obj.length; index++) { const child = obj[index]; - if (typeof child === "string") { + if (typeof child === "string") { replaceObjectProperty(obj, index, keyword, value); } else if (typeof child === "object") { valueInsertion(child, keyword, value); } } } else if (typeof obj === "object") { + unsafeAssert>(obj); for(let key in obj) { if (!obj[key]) continue; if (typeof obj[key] === "string") { - replaceObjectProperty(obj, key, keyword, value); + replaceObjectProperty(obj as Record, key, keyword, value); } else { valueInsertion(obj[key], keyword, value); } @@ -344,7 +340,7 @@ appliers["FOR_IN"] = async function (state) { for(let i = 0; i < values.length; i++) { const cloneBody = photocopy(body); const value = values[i]; - valueInsertion(cloneBody, keyword, value); + valueInsertion(cloneBody, keyword, value as { [replacementId: string]: string | number; }); state.debugState.addStep(i, 'VALUE_INDEX'); for (let index = 0; index < cloneBody.length; index++) { const statement = cloneBody[index]; @@ -379,17 +375,14 @@ appliers["PASTE"] = async function(state) { if (Array.isArray(state.currentValue)) { const obj = { type: "ADD_ARRAY_ELEMENT", - content: value + content: value, + ...(typeof this["index"] === 'number' && !isNaN(this["index"]) ? {index: this["index"]} : {}) }; - - if (!isNaN(this["index"])) { - obj.index = this["index"]; - } - await applyStep(obj, state); + await applyStep(obj as PatchStep.ADD_ARRAY_ELEMENT, state); } else if (typeof state.currentValue === "object") { await applyStep({ type: "SET_KEY", - index: this["index"], + index: this["index"]!, content: value }, state); } else { @@ -409,24 +402,23 @@ appliers["ENTER"] = async function (state) { state.debugState.throwError('Error', 'index must be set.'); } - let path = [this["index"]]; - if (this["index"].constructor == Array) - path = this["index"]; + const path = Array.isArray(this["index"]) ? this["index"] : [this["index"]]; for (let i = 0; i < path.length;i++) { const idx = path[i]; + unsafeAssert(state.currentValue); state.stack.push(state.currentValue); - if (state.currentValue[idx] === undefined) { + if (state.currentValue[idx as keyof StackEntry] === undefined) { const subArr = path.slice(0, i + 1); state.debugState.throwError('Error', `index sequence ${subArr.join(",")} leads to an undefined state.`); } - - state.currentValue = state.currentValue[idx]; + + state.currentValue = state.currentValue[idx as keyof StackEntry]; } }; appliers["EXIT"] = async function (state) { let count = 1; - if ("count" in this) + if (this["count"] !== undefined) count = this["count"]; for (let i = 0; i < count; i++) { if (state.stack.length === 0) { @@ -441,6 +433,8 @@ appliers["SET_KEY"] = async function (state) { state.debugState.throwError('Error', 'index must be set.'); } + unsafeAssert>(state.currentValue); + if ("content" in this) { state.currentValue[this["index"]] = photocopy(this["content"]); } else { @@ -449,11 +443,14 @@ appliers["SET_KEY"] = async function (state) { }; appliers["REMOVE_ARRAY_ELEMENT"] = async function (state) { + unsafeAssert(state.currentValue); state.currentValue.splice(this["index"], 1); }; appliers["ADD_ARRAY_ELEMENT"] = async function (state) { - if ("index" in this) { + unsafeAssert(state.currentValue); + + if (this["index"] !== undefined) { state.currentValue.splice(this["index"], 0, photocopy(this["content"])); } else { state.currentValue.push(photocopy(this["content"])); @@ -461,7 +458,7 @@ appliers["ADD_ARRAY_ELEMENT"] = async function (state) { }; // Reintroduced but simplified version of Emileyah's resolveUrl -function parsePath(url, fromGame) { +function parsePath(url: string, fromGame: boolean): [(boolean | string), string] { try { const decomposedUrl = new URL(url); const protocol = decomposedUrl.protocol; @@ -496,16 +493,17 @@ appliers["IMPORT"] = async function (state) { const srcPath = parsePath(this["src"], true); let obj = await state.loader.apply(state, srcPath); - if ("path" in this) { + if (this["path"] !== undefined) { if (!Array.isArray(this["path"])) { state.debugState.throwError('ValueError', 'path must be an array.'); } for (let i = 0; i < this["path"].length; i++) - obj = obj[this["path"][i]]; + obj = (obj as Record)[this["path"][i]]; } - if ("index" in this) { - state.currentValue[this["index"]] = photocopy(obj); + if (this["index"] !== undefined) { + unsafeAssert>(state.currentValue); + state.currentValue![this["index"]] = photocopy(obj); } else { photomerge(state.currentValue, obj); } @@ -520,7 +518,7 @@ appliers["INCLUDE"] = async function (state) { const data = await state.loader.apply(state, srcPath); state.debugState.addFile(srcPath); - await patch(state.currentValue, data, state.loader, state.debugState); + await patch(state.currentValue, data as PatchFile, state.loader, state.debugState); state.debugState.removeLastFile(); }; @@ -529,7 +527,9 @@ appliers["INIT_KEY"] = async function (state) { state.debugState.throwError('ValueError', 'index must be set.'); } - if (!(this["index"] in state.currentValue)) + unsafeAssert>(state.currentValue); + + if (!(this["index"] in (state.currentValue))) state.currentValue[this["index"]] = photocopy(this["content"]); }; @@ -544,4 +544,4 @@ appliers["MERGE_CONTENT"] = async function (state) { } photomerge(state.currentValue, this["content"]); -} +}; diff --git a/common/vendor-libs/patch-steps-lib/src/patchsteps-utils.js b/common/vendor-libs/patch-steps-lib/src/patchsteps-utils.ts similarity index 64% rename from common/vendor-libs/patch-steps-lib/src/patchsteps-utils.js rename to common/vendor-libs/patch-steps-lib/src/patchsteps-utils.ts index 5a03aec..fcbfd50 100644 --- a/common/vendor-libs/patch-steps-lib/src/patchsteps-utils.js +++ b/common/vendor-libs/patch-steps-lib/src/patchsteps-utils.ts @@ -10,29 +10,29 @@ /** * A generic merge function. * NOTE: This should match Patch Steps specification, specifically how IMPORT merging works. - * @param {any} a The value to merge into. - * @param {any} b The value to merge from. - * @returns {any} a + * @param a The value to merge into. + * @param b The value to merge from. + * @returns a */ -export function photomerge(a, b) { - if (b.constructor === Object) { - for (let k in b) - a[photocopy(k)] = photocopy(b[k]); - } else if (b.constructor == Array) { +export function photomerge(a: A, b: B): A & B { + if (Array.isArray(b)) { for (let i = 0; i < b.length; i++) - a.push(photocopy(b[i])); - } else { + (a as any[]).push(photocopy(b[i])); + } else if (b instanceof Object) { + for (let k in b) + (a as Record)[photocopy(k)] = photocopy((b as Record)[k]); + } else { throw new Error("We can't do that! ...Who'd clean up the mess?"); } - return a; + return a as A & B; } /** * A generic copy function. - * @param {any} a The value to copy. - * @returns {any} copied value + * @param o The value to copy. + * @returns copied value */ -export function photocopy(o) { +export function photocopy(o: O): O { if (o) { if (o.constructor === Array) return photomerge([], o); diff --git a/common/vendor-libs/patch-steps-lib/src/patchsteps.js b/common/vendor-libs/patch-steps-lib/src/patchsteps.ts similarity index 100% rename from common/vendor-libs/patch-steps-lib/src/patchsteps.js rename to common/vendor-libs/patch-steps-lib/src/patchsteps.ts diff --git a/common/vendor-libs/patch-steps-lib/src/types.ts b/common/vendor-libs/patch-steps-lib/src/types.ts new file mode 100644 index 0000000..77fd8e2 --- /dev/null +++ b/common/vendor-libs/patch-steps-lib/src/types.ts @@ -0,0 +1,174 @@ +import {DebugState} from "./patchsteps-patch.js"; + +export type Index = string | number; + +export interface BasePatchStep { + comment?: string; +} + +export namespace PatchStep { + export interface ENTER extends BasePatchStep { + type: 'ENTER'; + index: Index | Index[]; + } + + export interface EXIT extends BasePatchStep { + type: 'EXIT'; + count?: number; + } + + export interface SET_KEY extends BasePatchStep { + type: 'SET_KEY'; + index: Index; + content?: unknown; + } + + export interface INIT_KEY extends BasePatchStep { + type: 'INIT_KEY'; + index: Index; + content: unknown; + } + + export interface REMOVE_ARRAY_ELEMENT extends BasePatchStep { + type: 'REMOVE_ARRAY_ELEMENT'; + index: number; + } + + export interface ADD_ARRAY_ELEMENT extends BasePatchStep { + type: 'ADD_ARRAY_ELEMENT'; + index?: number; + content: unknown; + } + + export interface IMPORT extends BasePatchStep { + type: 'IMPORT'; + src: string; + path?: Index[]; + index?: Index; + } + + export interface INCLUDE extends BasePatchStep { + type: 'INCLUDE'; + src: string; + } + + export interface FOR_IN extends BasePatchStep { + type: 'FOR_IN'; + values: unknown[] | Array>; + keyword: string | Record; + body: AnyPatchStep[]; + } + + export interface COPY extends BasePatchStep { + type: 'COPY'; + alias: string; + } + + export interface PASTE extends BasePatchStep { + type: 'PASTE'; + alias: string; + index?: Index; + } + + export interface COMMENT extends BasePatchStep { + type: 'COMMENT'; + value: unknown; + } + + export interface DEBUG extends BasePatchStep { + type: 'DEBUG'; + value: boolean; + } + + export interface MERGE_CONTENT extends BasePatchStep { + type: 'MERGE_CONTENT'; + content: unknown; + } + + export interface CALL extends BasePatchStep { + type: 'CALL'; + id: string; + args: unknown; + } +} + +export interface PatchStepsRegistry { + ENTER: PatchStep.ENTER; + EXIT: PatchStep.EXIT; + SET_KEY: PatchStep.SET_KEY; + INIT_KEY: PatchStep.INIT_KEY; + REMOVE_ARRAY_ELEMENT: PatchStep.REMOVE_ARRAY_ELEMENT; + ADD_ARRAY_ELEMENT: PatchStep.ADD_ARRAY_ELEMENT; + IMPORT: PatchStep.IMPORT; + INCLUDE: PatchStep.INCLUDE; + FOR_IN: PatchStep.FOR_IN; + COPY: PatchStep.COPY; + PASTE: PatchStep.PASTE; + COMMENT: PatchStep.COMMENT; + DEBUG: PatchStep.DEBUG; + MERGE_CONTENT: PatchStep.MERGE_CONTENT; + CALL: PatchStep.CALL; +} + +export type AnyPatchStep = Extract; +export type PatchFile = AnyPatchStep[] | Record; + +export type ParsedPath = null | [fromGame: true | false | string, url: string]; + +// The following are definitions used for reference in DebugState. +/* + * ParsedPath is actually any type that translateParsedPath can understand. + * And translateParsedPath can be overridden by the user. + * But the types declared here are those that will be received no matter what. + */ +export interface FileInfo { + path: string; + stack: StackEntry[]; +} + +export interface StackEntryError { + type: "Error"; + errorType: string; + errorMessage: string; +} +export interface StackEntryStep { + type: "Step", + index: Index; + name: string; +} +export type StackEntry = StackEntryStep | StackEntryError; + +export type Loader = (fromGame: boolean | string, path: string) => Promise; + +export interface ApplierState { + currentValue: unknown; + stack: StackEntry[]; + cloneMap: Map; + loader: Loader; + debugState: DebugState; + debug: boolean; +} + +export type Applier = (this: T, state: ApplierState) => Promise; +export type Appliers = { + [K in keyof PatchStepsRegistry]: Applier +} + +export type DiffCore = (a: unknown, b: unknown, settings: DiffSettings) => AnyPatchStep[] | null; + +export interface DiffSettings { + arrayTrulyDifferentThreshold: number; + trulyDifferentThreshold: number; + arrayLookahead: number; + diffAddNewKey: number; + diffAddDelKey: number; + diffMulSameKey: number; + + diffCore: DiffCore; + comment?: string; + commentValue?: string; + path: Index[]; + optimize: boolean; +} + +export function unsafeAssert(val: any): asserts val is T {}