All files / src/lib/scriptEventsHandlers handlerCommon.ts

100% Statements 80/80
100% Branches 25/25
100% Functions 18/18
100% Lines 69/69

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                            9x 9x 9x 9x 9x 9x 9x 9x         9x     13475x                                   9x   9x 13475x 13475x           9x       13459x           8x                 9x       18277x 55958x 4826x 51132x   46416x 46416x   46416x               9x 13451x   368x   368x 1100x       5476x   368x 2x     2x             9x               13451x                                                             13451x 38908x     13451x 45130x 45130x       13451x           9x 13451x 13451x     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(", "));
  }
};
 
/**
 * 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;
};