| 1 | import { readFile, copyFile } from "node:fs/promises"; |
| 2 | import { createRequire } from "node:module"; |
| 3 | import { dirname, join } from "node:path"; |
| 4 | import { runInNewContext } from "node:vm"; |
| 5 | import ts from "typescript"; |
| 6 | |
| 7 | export async function playwrightPageResource(dist) { |
| 8 | const require = createRequire(import.meta.url); |
| 9 | const core = createRequire(require.resolve("playwright/package.json")).resolve("playwright-core/package.json"); |
| 10 | const version = JSON.parse(await readFile(core, "utf8")).version; |
| 11 | if (version !== "1.62.1") throw new Error(`Unqualified Playwright injected runtime version: ${version}`); |
| 12 | const file = ts.createSourceFile("coreBundle.js", await readFile(join(dirname(core), "lib/coreBundle.js"), "utf8"), ts.ScriptTarget.Latest, true); |
| 13 | const candidates = []; |
| 14 | function visit(node) { |
| 15 | if (ts.isStringLiteral(node) && node.text.includes("injectedScript_exports") && node.text.includes("generateAriaTree")) candidates.push(node.text); |
| 16 | ts.forEachChild(node, visit); |
| 17 | } |
| 18 | visit(file); |
| 19 | if (candidates.length !== 1) throw new Error("Playwright injected resource export contract changed"); |
| 20 | let source = candidates[0]; |
| 21 | // 1.62.1 serializes text-input values. Never expose password values, including |
| 22 | // an explicitly role=textbox password. This reviewed adapter is version bound. |
| 23 | const valueCondition = 'element.type !== "checkbox" && element.type !== "radio" && element.type !== "file"'; |
| 24 | if (source.split(valueCondition).length !== 2) throw new Error("Playwright password redaction contract changed"); |
| 25 | source = source.replace(valueCondition, `${valueCondition} && element.type !== "password"`); |
| 26 | // The generated export table contains getter functions, not constructors. |
| 27 | source += "\nmodule.exports = { InjectedScript: module.exports.InjectedScript() };\n"; |
| 28 | const module = { exports: {} }; |
| 29 | runInNewContext(source, { module }, { timeout: 1000 }); |
| 30 | const prototype = module.exports.InjectedScript?.prototype; |
| 31 | for (const method of ["ariaSnapshotWithRefs", "parseSelector", "querySelectorAll", "elementState"]) { |
| 32 | if (typeof prototype?.[method] !== "function") throw new Error(`Playwright contract missing ${method}`); |
| 33 | } |
| 34 | await copyFile(join(dirname(core), "LICENSE"), join(dist, "browser-playwright-LICENSE")); |
| 35 | await copyFile(join(dirname(core), "NOTICE"), join(dist, "browser-playwright-NOTICE")); |
| 36 | return { version, source }; |
| 37 | } |
| 38 |