All files / src/lib/scriptEventsHandlers handlerCommon.ts

97.67% Statements 84/86
96.43% Branches 27/28
100% Functions 19/19
97.33% Lines 73/75

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260                            9x 9x 9x 9x 9x 9x 9x 9x         9x     16081x                                   9x   9x 16081x 16081x           9x       16065x           8x                 9x       22220x 67663x 6163x 61500x   55682x 55682x   55682x               9x 16057x   654x   654x 1958x       8024x   654x 2x     2x     654x   654x 8240x     654x                     9x               16057x                                                             16057x 46545x     16057x 53940x 53940x       16057x           9x 16057x 16057x     9x 2x           9x 145x     145x           145x     2x   6x 6x 2x         145x     145x     77x 77x 75x   2x           145x 37x 77x   37x       145x     6x       145x         145x    
/**
 * Shared Handler Utilities
 *
 * Common functionality used by both secure VM and trusted execution handlers.
 */
 
import type {
  FileReaderFn,
  ScriptEventHandler,
  ScriptEventHandlerFieldSchema,
  ScriptEventHelperDef,
  ScriptEventPresetValue,
  UserPresetsGroup,
} from "lib/scriptEventsHandlers/handlerTypes";
import * as eventHelpers from "lib/events/helpers";
import * as eventSystemHelpers from "lib/helpers/eventSystem";
import * as compileEntityEvents from "lib/compiler/compileEntityEvents";
import * as scriptValueTypes from "shared/lib/scriptValue/types";
import * as scriptValueHelpers from "shared/lib/scriptValue/helpers";
import * as l10n from "shared/lib/lang/l10n";
import trimLines from "shared/lib/helpers/trimlines";
import { createScriptEventHandlerAPI } from "./handlerApi";
 
/**
 * Creates the standard set of mock dependencies available to handler code.
 */
export const createMockRequire = (
  readFile: FileReaderFn,
): Record<string, unknown> => {
  return {
    "plugin-api": createScriptEventHandlerAPI(readFile),
    // Legacy compatibility for older plugins
    "./helpers": eventHelpers,
    "../helpers/l10n": l10n,
    "../helpers/eventSystem": eventSystemHelpers,
    "../compiler/compileEntityEvents": compileEntityEvents,
    "../helpers/trimlines": trimLines,
    "shared/lib/helpers/trimlines": trimLines,
    "shared/lib/scriptValue/helpers": scriptValueHelpers,
    "shared/lib/scriptValue/types": scriptValueTypes,
  };
};
 
/**
 * Analyzes JavaScript code to determine module system (ES modules vs CommonJS).
 */
const ESM_RE =
  /(^|\n|\r)\s*import\s+(?!\()|(^|\n|\r)\s*export\s+(?:\*|default|{|\w)/m;
 
export const detectModuleSystem = (code: string): "module" | "global" => {
  const isESM = ESM_RE.test(code);
  return isESM ? "module" : "global";
};
 
/**
 * Validates that the loaded handler exports the required properties.
 */
export const validateHandlerMetadata = (
  metadata: unknown,
  filename: string,
): void => {
  if (
    !metadata ||
    typeof metadata !== "object" ||
    !("id" in metadata) ||
    typeof metadata.id !== "string"
  ) {
    throw new Error(
      `Script event handler at ${filename} does not export an 'id' property.`,
    );
  }
};
 
/**
 * Creates a flat lookup table for efficient field access during editor runtime.
 */
export const buildFieldsLookup = (
  handler: ScriptEventHandler,
  fields: ScriptEventHandlerFieldSchema[],
): void => {
  for (const field of fields) {
    if (field.type === "group" && field.fields) {
      buildFieldsLookup(handler, field.fields);
    } else if (field.key) {
      // Mark fields that have post-update callbacks for renderer optimization
      const fieldRecord = field as Record<string, unknown>;
      (field as ScriptEventHandlerFieldSchema).hasPostUpdateFn =
        !!fieldRecord.postUpdateFn;
      handler.fieldsLookup[field.key] = field;
    }
  }
};
 
/**
 * Validates that user preset groups account for all handler fields.
 */
export const validateUserPresets = (handler: ScriptEventHandler): void => {
  if (!handler.userPresetsGroups) return;
 
  const allFields = Object.keys(handler.fieldsLookup);
 
  const presetFields = handler.userPresetsGroups
    .map((group: { fields: string[] }) => group.fields)
    .flat()
    .concat(handler.userPresetsIgnore ?? []);
 
  const missingFields = allFields.filter((key) => !presetFields.includes(key));
 
  if (missingFields.length > 0) {
    console.error(
      `${handler.id} defined userPresetsGroups but did not include some fields in either userPresetsGroups or userPresetsIgnore`,
    );
    console.error("Missing fields: " + missingFields.join(", "));
  }
 
  const allowedExtraFields = ["units"];
 
  const extraFields = presetFields.filter(
    (key) => !allFields.includes(key) && !allowedExtraFields.includes(key),
  );
 
  Iif (extraFields.length > 0) {
    console.error(
      `${handler.id} defined userPresetsGroups or userPresetsIgnore but included some fields that do not exist in the handler's fields.`,
    );
    console.error("Extra fields: " + extraFields.join(", "));
  }
};
 
/**
 * Creates the base handler object structure with all required properties.
 */
export const createHandlerBase = (
  metadata: Record<string, unknown>,
  hasAutoLabelFunction: boolean,
  cleanup: () => void,
): Omit<
  ScriptEventHandler & { cleanup: () => void },
  "autoLabel" | "compile"
> => {
  const handler = {
    ...metadata,
    id: metadata.id as string,
    fields: Array.isArray(metadata.fields)
      ? (metadata.fields as ScriptEventHandlerFieldSchema[])
      : [],
    cleanup,
    hasAutoLabel: hasAutoLabelFunction,
    fieldsLookup: {} as Record<string, ScriptEventHandlerFieldSchema>,
    name: metadata.name as string | undefined,
    description: metadata.description as string | undefined,
    groups: metadata.groups as string[] | string | undefined,
    subGroups: metadata.subGroups as Record<string, string> | undefined,
    deprecated: metadata.deprecated as boolean | undefined,
    isConditional: false,
    editableSymbol: metadata.editableSymbol as boolean | undefined,
    allowChildrenBeforeInitFade: metadata.allowChildrenBeforeInitFade as
      | boolean
      | undefined,
    waitUntilAfterInitFade: metadata.waitUntilAfterInitFade as
      | boolean
      | undefined,
    helper: metadata.helper as ScriptEventHelperDef | undefined,
    presets: metadata.presets as ScriptEventPresetValue[] | undefined,
    userPresetsGroups: metadata.userPresetsGroups as
      | UserPresetsGroup[]
      | undefined,
    userPresetsIgnore: metadata.userPresetsIgnore as string[] | undefined,
  };
 
  // Set conditional flag based on presence of events fields
  handler.isConditional =
    handler.fields && !!handler.fields.find((f) => f.type === "events");
 
  // Mark fields that have post-update callbacks for renderer optimization
  for (const field of handler.fields) {
    const fieldRecord = field as Record<string, unknown>;
    (field as ScriptEventHandlerFieldSchema).hasPostUpdateFn =
      !!fieldRecord.postUpdateFn;
  }
 
  return handler;
};
 
/**
 * Assembles a complete handler by building lookup tables and validating configuration.
 */
export const finalizeHandler = (handler: ScriptEventHandler): void => {
  buildFieldsLookup(handler, handler.fields);
  validateUserPresets(handler);
};
 
export const noReadFileFn: FileReaderFn = (_path: string): string => {
  throw new Error("This handler is not configured to read files.");
};
 
/**
 * Converts ES modules to CommonJS for eval compatibility.
 */
export const convertESMToCommonJS = (code: string): string => {
  let convertedCode = code;
 
  // Convert export default statements
  convertedCode = convertedCode.replace(
    /export\s+default\s+(.+?)(?:;|$)/gm,
    "module.exports = $1;",
  );
 
  // Convert named exports
  convertedCode = convertedCode.replace(
    /export\s*\{\s*([^}]+)\s*\}\s*;?/gm,
    (match, exports) => {
      const exportList = exports
        .split(",")
        .map((exp: string) => exp.trim())
        .filter((exp: string) => exp);
      return `module.exports = { ${exportList.join(", ")} };`;
    },
  );
 
  // Track individual exports to build a combined module.exports
  const individualExports: string[] = [];
 
  // Convert individual export declarations like "export const id = 'value'"
  convertedCode = convertedCode.replace(
    /export\s+(const|let|var|function|class)\s+(\w+)(\s*=\s*[^;]+)?/gm,
    (match, keyword, name, assignment) => {
      individualExports.push(name);
      if (assignment) {
        return `${keyword} ${name}${assignment}`;
      } else {
        return `${keyword} ${name}`;
      }
    },
  );
 
  // If we found individual exports, add them to module.exports
  if (individualExports.length > 0) {
    const exportsAssignment = individualExports
      .map((name) => `${name}`)
      .join(", ");
    convertedCode += `\nmodule.exports = { ${exportsAssignment} };`;
  }
 
  // Convert import statements to require calls
  convertedCode = convertedCode.replace(
    /import\s+(\w+)\s+from\s+['"](.+?)['"];?\s*$/gm,
    (match, varName, moduleName) => {
      return `const _mod_${varName} = require("${moduleName}"); const ${varName} = _mod_${varName}.default || _mod_${varName}.${varName} || _mod_${varName};`;
    },
  );
 
  convertedCode = convertedCode.replace(
    /import\s*\{\s*([^}]+)\s*\}\s*from\s+['"](.+?)['"];?\s*$/gm,
    'const { $1 } = require("$2");',
  );
 
  return convertedCode;
};