summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/script/script-command.js
blob: 93917944d562e734b0d3f479c0c582749fcc0064 (plain)
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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {
  getAdHocFrontOrPrimitiveGrip,
  // eslint-disable-next-line mozilla/reject-some-requires
} = require("resource://devtools/client/fronts/object.js");

class ScriptCommand {
  constructor({ commands }) {
    this._commands = commands;
  }

  /**
   * Execute a JavaScript expression.
   *
   * @param {String} expression: The code you want to evaluate.
   * @param {Object} options: Options for evaluation:
   * @param {Object} options.frameActor: a FrameActor ID. The actor holds a reference to
   *        a Debugger.Frame. This option allows you to evaluate the string in the frame
   *        of the given FrameActor.
   * @param {String} options.url: the url to evaluate the script as. Defaults to "debugger eval code".
   * @param {TargetFront} options.selectedTargetFront: When passed, the expression will be
   *        evaluated in the context of the target (as opposed to the default, top-level one).
   * @param {String} options.selectedNodeActor: A NodeActor ID that may be used by helper
   *        functions that can reference the currently selected node in the Inspector, like $0.
   * @param {String} options.selectedObjectActor: the actorID of a given objectActor.
   *        This is used by context menu entries to get a reference to an object, in order
   *        to perform some operation on it (copy it, store it as a global variable, …).
   * @param {Number} options.innerWindowID: An optional window id to be used for the evaluation,
   *        instead of the regular webConsoleActor.evalWindow.
   *        This is used by functions that may want to evaluate in a different window (for
   *        example a non-remote iframe), like getting the elements of a given document.
   * @param {object} options.mapped: An optional object indicating if the original expression
   *        entered by the users have been modified
   * @param {boolean} options.mapped.await: true if the expression was a top-level await
   *        expression that was wrapped in an async-iife
   *
   * @return {Promise}: A promise that resolves with the response.
   */
  async execute(expression, options = {}) {
    const {
      selectedObjectActor,
      selectedNodeActor,
      frameActor,
      selectedTargetFront,
    } = options;

    // Retrieve the right WebConsole front that relates either to (by order of priority):
    // - the currently selected target in the context selector
    //   (selectedTargetFront argument),
    // - the object picked in the console (when using store as global) (selectedObjectActor),
    // - the currently selected Node in the inspector (selectedNodeActor),
    // - the currently selected frame in the debugger (when paused) (frameActor),
    // - the currently selected target in the iframe dropdown
    //   (selectedTargetFront from the TargetCommand)
    let targetFront = this._commands.targetCommand.selectedTargetFront;

    const selectedActor =
      selectedObjectActor || selectedNodeActor || frameActor;

    if (selectedTargetFront) {
      targetFront = selectedTargetFront;
    } else if (selectedActor) {
      const selectedFront = this._commands.client.getFrontByID(selectedActor);
      if (selectedFront) {
        targetFront = selectedFront.targetFront;
      }
    }

    const consoleFront = await targetFront.getFront("console");

    // We call `evaluateJSAsync` RDP request, which immediately returns a simple `resultID`,
    // for which we later receive a related `evaluationResult` RDP event, with the same resultID.
    // The evaluation result will be contained in this RDP event.
    let resultID;
    const response = await new Promise(resolve => {
      const offEvaluationResult = consoleFront.on(
        "evaluationResult",
        async packet => {
          // In some cases, the evaluationResult event can be received before the call to
          // evaluationJSAsync completes. So make sure to wait for the corresponding promise
          // before handling the evaluationResult event.
          await onEvaluateJSAsync;

          if (packet.resultID === resultID) {
            resolve(packet);
            offEvaluationResult();
          }
        }
      );

      const onEvaluateJSAsync = consoleFront
        .evaluateJSAsync({
          text: expression,
          eager: options.eager,
          frameActor,
          innerWindowID: options.innerWindowID,
          mapped: options.mapped,
          selectedNodeActor,
          selectedObjectActor,
          url: options.url,
        })
        .then(packet => {
          resultID = packet.resultID;
        });
    });

    // `response` is the packet sent via `evaluationResult` RDP event.
    if (response.error) {
      throw response;
    }

    if (response.result) {
      response.result = getAdHocFrontOrPrimitiveGrip(
        response.result,
        consoleFront
      );
    }

    if (response.helperResult?.object) {
      response.helperResult.object = getAdHocFrontOrPrimitiveGrip(
        response.helperResult.object,
        consoleFront
      );
    }

    if (response.exception) {
      response.exception = getAdHocFrontOrPrimitiveGrip(
        response.exception,
        consoleFront
      );
    }

    if (response.exceptionMessage) {
      response.exceptionMessage = getAdHocFrontOrPrimitiveGrip(
        response.exceptionMessage,
        consoleFront
      );
    }

    return response;
  }
}

module.exports = ScriptCommand;