config.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var config_exports = {};
  30. __export(config_exports, {
  31. commaSeparatedList: () => commaSeparatedList,
  32. configFromCLIOptions: () => configFromCLIOptions,
  33. defaultConfig: () => defaultConfig,
  34. dotenvFileLoader: () => dotenvFileLoader,
  35. headerParser: () => headerParser,
  36. numberParser: () => numberParser,
  37. outputDir: () => outputDir,
  38. outputFile: () => outputFile,
  39. resolutionParser: () => resolutionParser,
  40. resolveCLIConfig: () => resolveCLIConfig,
  41. resolveConfig: () => resolveConfig
  42. });
  43. module.exports = __toCommonJS(config_exports);
  44. var import_fs = __toESM(require("fs"));
  45. var import_os = __toESM(require("os"));
  46. var import_path = __toESM(require("path"));
  47. var import_playwright_core = require("playwright-core");
  48. var import_utilsBundle = require("playwright-core/lib/utilsBundle");
  49. var import_util = require("../../util");
  50. var import_server = require("../sdk/server");
  51. const defaultConfig = {
  52. browser: {
  53. browserName: "chromium",
  54. launchOptions: {
  55. channel: "chrome",
  56. headless: import_os.default.platform() === "linux" && !process.env.DISPLAY,
  57. chromiumSandbox: true
  58. },
  59. contextOptions: {
  60. viewport: null
  61. }
  62. },
  63. server: {},
  64. saveTrace: false,
  65. timeouts: {
  66. action: 5e3,
  67. navigation: 6e4
  68. }
  69. };
  70. async function resolveConfig(config) {
  71. return mergeConfig(defaultConfig, config);
  72. }
  73. async function resolveCLIConfig(cliOptions) {
  74. const configInFile = await loadConfig(cliOptions.config);
  75. const envOverrides = configFromEnv();
  76. const cliOverrides = configFromCLIOptions(cliOptions);
  77. let result = defaultConfig;
  78. result = mergeConfig(result, configInFile);
  79. result = mergeConfig(result, envOverrides);
  80. result = mergeConfig(result, cliOverrides);
  81. await validateConfig(result);
  82. return result;
  83. }
  84. async function validateConfig(config) {
  85. if (config.browser.initScript) {
  86. for (const script of config.browser.initScript) {
  87. if (!await (0, import_util.fileExistsAsync)(script))
  88. throw new Error(`Init script file does not exist: ${script}`);
  89. }
  90. }
  91. if (config.sharedBrowserContext && config.saveVideo)
  92. throw new Error("saveVideo is not supported when sharedBrowserContext is true");
  93. }
  94. function configFromCLIOptions(cliOptions) {
  95. let browserName;
  96. let channel;
  97. switch (cliOptions.browser) {
  98. case "chrome":
  99. case "chrome-beta":
  100. case "chrome-canary":
  101. case "chrome-dev":
  102. case "chromium":
  103. case "msedge":
  104. case "msedge-beta":
  105. case "msedge-canary":
  106. case "msedge-dev":
  107. browserName = "chromium";
  108. channel = cliOptions.browser;
  109. break;
  110. case "firefox":
  111. browserName = "firefox";
  112. break;
  113. case "webkit":
  114. browserName = "webkit";
  115. break;
  116. }
  117. const launchOptions = {
  118. channel,
  119. executablePath: cliOptions.executablePath,
  120. headless: cliOptions.headless
  121. };
  122. if (cliOptions.sandbox === false)
  123. launchOptions.chromiumSandbox = false;
  124. if (cliOptions.proxyServer) {
  125. launchOptions.proxy = {
  126. server: cliOptions.proxyServer
  127. };
  128. if (cliOptions.proxyBypass)
  129. launchOptions.proxy.bypass = cliOptions.proxyBypass;
  130. }
  131. if (cliOptions.device && cliOptions.cdpEndpoint)
  132. throw new Error("Device emulation is not supported with cdpEndpoint.");
  133. const contextOptions = cliOptions.device ? import_playwright_core.devices[cliOptions.device] : {};
  134. if (cliOptions.storageState)
  135. contextOptions.storageState = cliOptions.storageState;
  136. if (cliOptions.userAgent)
  137. contextOptions.userAgent = cliOptions.userAgent;
  138. if (cliOptions.viewportSize)
  139. contextOptions.viewport = cliOptions.viewportSize;
  140. if (cliOptions.ignoreHttpsErrors)
  141. contextOptions.ignoreHTTPSErrors = true;
  142. if (cliOptions.blockServiceWorkers)
  143. contextOptions.serviceWorkers = "block";
  144. if (cliOptions.grantPermissions)
  145. contextOptions.permissions = cliOptions.grantPermissions;
  146. if (cliOptions.saveVideo) {
  147. contextOptions.recordVideo = {
  148. // Videos are moved to output directory on saveAs.
  149. dir: tmpDir(),
  150. size: cliOptions.saveVideo
  151. };
  152. }
  153. const result = {
  154. browser: {
  155. browserName,
  156. isolated: cliOptions.isolated,
  157. userDataDir: cliOptions.userDataDir,
  158. launchOptions,
  159. contextOptions,
  160. cdpEndpoint: cliOptions.cdpEndpoint,
  161. cdpHeaders: cliOptions.cdpHeader,
  162. initPage: cliOptions.initPage,
  163. initScript: cliOptions.initScript
  164. },
  165. server: {
  166. port: cliOptions.port,
  167. host: cliOptions.host,
  168. allowedHosts: cliOptions.allowedHosts
  169. },
  170. capabilities: cliOptions.caps,
  171. saveSession: cliOptions.saveSession,
  172. saveTrace: cliOptions.saveTrace,
  173. saveVideo: cliOptions.saveVideo,
  174. secrets: cliOptions.secrets,
  175. sharedBrowserContext: cliOptions.sharedBrowserContext,
  176. outputDir: cliOptions.outputDir,
  177. imageResponses: cliOptions.imageResponses,
  178. testIdAttribute: cliOptions.testIdAttribute,
  179. timeouts: {
  180. action: cliOptions.timeoutAction,
  181. navigation: cliOptions.timeoutNavigation
  182. }
  183. };
  184. return result;
  185. }
  186. function configFromEnv() {
  187. const options = {};
  188. options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
  189. options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
  190. options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
  191. options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
  192. options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
  193. options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
  194. options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
  195. options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
  196. options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
  197. options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
  198. options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
  199. options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
  200. options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
  201. const initPage = envToString(process.env.PLAYWRIGHT_MCP_INIT_PAGE);
  202. if (initPage)
  203. options.initPage = [initPage];
  204. const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
  205. if (initScript)
  206. options.initScript = [initScript];
  207. options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
  208. if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === "omit")
  209. options.imageResponses = "omit";
  210. options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
  211. options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
  212. options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
  213. options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
  214. options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
  215. options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
  216. options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
  217. options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
  218. options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
  219. options.testIdAttribute = envToString(process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
  220. options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
  221. options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
  222. options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
  223. options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
  224. options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
  225. return configFromCLIOptions(options);
  226. }
  227. async function loadConfig(configFile) {
  228. if (!configFile)
  229. return {};
  230. try {
  231. return JSON.parse(await import_fs.default.promises.readFile(configFile, "utf8"));
  232. } catch (error) {
  233. throw new Error(`Failed to load config file: ${configFile}, ${error}`);
  234. }
  235. }
  236. function tmpDir() {
  237. return import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output");
  238. }
  239. function outputDir(config, clientInfo) {
  240. const rootPath = (0, import_server.firstRootPath)(clientInfo);
  241. return config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(tmpDir(), String(clientInfo.timestamp));
  242. }
  243. async function outputFile(config, clientInfo, fileName, options) {
  244. const file = await resolveFile(config, clientInfo, fileName, options);
  245. (0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
  246. return file;
  247. }
  248. async function resolveFile(config, clientInfo, fileName, options) {
  249. const dir = outputDir(config, clientInfo);
  250. if (options.origin === "code")
  251. return import_path.default.resolve(dir, fileName);
  252. if (options.origin === "llm") {
  253. fileName = fileName.split("\\").join("/");
  254. const resolvedFile = import_path.default.resolve(dir, fileName);
  255. if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
  256. throw new Error(`Resolved file path ${resolvedFile} is outside of the output directory ${dir}. Use relative file names to stay within the output directory.`);
  257. return resolvedFile;
  258. }
  259. return import_path.default.join(dir, sanitizeForFilePath(fileName));
  260. }
  261. function pickDefined(obj) {
  262. return Object.fromEntries(
  263. Object.entries(obj ?? {}).filter(([_, v]) => v !== void 0)
  264. );
  265. }
  266. function mergeConfig(base, overrides) {
  267. const browser = {
  268. ...pickDefined(base.browser),
  269. ...pickDefined(overrides.browser),
  270. browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? "chromium",
  271. isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
  272. launchOptions: {
  273. ...pickDefined(base.browser?.launchOptions),
  274. ...pickDefined(overrides.browser?.launchOptions),
  275. ...{ assistantMode: true }
  276. },
  277. contextOptions: {
  278. ...pickDefined(base.browser?.contextOptions),
  279. ...pickDefined(overrides.browser?.contextOptions)
  280. }
  281. };
  282. if (browser.browserName !== "chromium" && browser.launchOptions)
  283. delete browser.launchOptions.channel;
  284. return {
  285. ...pickDefined(base),
  286. ...pickDefined(overrides),
  287. browser,
  288. server: {
  289. ...pickDefined(base.server),
  290. ...pickDefined(overrides.server)
  291. },
  292. timeouts: {
  293. ...pickDefined(base.timeouts),
  294. ...pickDefined(overrides.timeouts)
  295. }
  296. };
  297. }
  298. function commaSeparatedList(value) {
  299. if (!value)
  300. return void 0;
  301. return value.split(",").map((v) => v.trim());
  302. }
  303. function dotenvFileLoader(value) {
  304. if (!value)
  305. return void 0;
  306. return import_utilsBundle.dotenv.parse(import_fs.default.readFileSync(value, "utf8"));
  307. }
  308. function numberParser(value) {
  309. if (!value)
  310. return void 0;
  311. return +value;
  312. }
  313. function resolutionParser(name, value) {
  314. if (!value)
  315. return void 0;
  316. if (value.includes("x")) {
  317. const [width, height] = value.split("x").map((v) => +v);
  318. if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
  319. throw new Error(`Invalid resolution format: use ${name}="800x600"`);
  320. return { width, height };
  321. }
  322. if (value.includes(",")) {
  323. const [width, height] = value.split(",").map((v) => +v);
  324. if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
  325. throw new Error(`Invalid resolution format: use ${name}="800x600"`);
  326. return { width, height };
  327. }
  328. throw new Error(`Invalid resolution format: use ${name}="800x600"`);
  329. }
  330. function headerParser(arg, previous) {
  331. if (!arg)
  332. return previous || {};
  333. const result = previous || {};
  334. const [name, value] = arg.split(":").map((v) => v.trim());
  335. result[name] = value;
  336. return result;
  337. }
  338. function envToBoolean(value) {
  339. if (value === "true" || value === "1")
  340. return true;
  341. if (value === "false" || value === "0")
  342. return false;
  343. return void 0;
  344. }
  345. function envToString(value) {
  346. return value ? value.trim() : void 0;
  347. }
  348. function sanitizeForFilePath(s) {
  349. const sanitize = (s2) => s2.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, "-");
  350. const separator = s.lastIndexOf(".");
  351. if (separator === -1)
  352. return sanitize(s);
  353. return sanitize(s.substring(0, separator)) + "." + sanitize(s.substring(separator + 1));
  354. }
  355. // Annotate the CommonJS export names for ESM import in node:
  356. 0 && (module.exports = {
  357. commaSeparatedList,
  358. configFromCLIOptions,
  359. defaultConfig,
  360. dotenvFileLoader,
  361. headerParser,
  362. numberParser,
  363. outputDir,
  364. outputFile,
  365. resolutionParser,
  366. resolveCLIConfig,
  367. resolveConfig
  368. });