All files / src/lib/scriptEventsHandlers handlerCommon.ts

100% Statements 57/57
100% Branches 22/22
100% Functions 11/11
100% Lines 47/47

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                            9x 9x 9x 9x 9x 9x 9x 9x         9x     12196x                                   9x   9x 12196x 12196x           9x       12179x           8x                 9x       16709x 50798x 4538x 46260x   42493x 42493x   42493x               9x 12171x   340x   340x 1016x       5056x   340x 2x     2x             9x               12171x                                                             12171x 34981x     12171x 40720x 40720x       12171x           9x 12171x 12171x     9x 2x    
/**
 * 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.");
};