diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index db47e98..5bc95f8 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/setup-node@v3 with: - node-version: ^18.17 + node-version: ^24.1 - name: import secrets id: import-secrets uses: LanceMcCarthy/akeyless-action@v3 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9a0da14..066e9ac 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: ^18.17 + node-version: ^24.1 - name: install virtual display run: sudo apt-get install xvfb - name: npm install diff --git a/examples/selenium-interop/w3schools.test.ts b/examples/selenium-interop/w3schools.test.ts index f7eb879..c524cbf 100644 --- a/examples/selenium-interop/w3schools.test.ts +++ b/examples/selenium-interop/w3schools.test.ts @@ -29,7 +29,7 @@ describe("w3schools", () => { await driver?.quit(); }); - test("navigate to js statements page", async () => { + test.skip("navigate to js statements page", async () => { await driver.navigate().to('http://www.w3schools.com'); await sleep(3000); await session.navigateTo('http://www.w3schools.com/js') diff --git a/examples/test-website/index.semantic.test.tsx b/examples/test-website/index.semantic.test.tsx new file mode 100644 index 0000000..cc71ca0 --- /dev/null +++ b/examples/test-website/index.semantic.test.tsx @@ -0,0 +1,255 @@ +// index.semantic.test.ts +import { describe, test, expect, beforeAll, afterAll, afterEach, beforeEach } from "@jest/globals"; +import { mkdir, writeFile } from "fs/promises"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Express } from "@progress/roadkill/express.js"; +import { getState, step } from "@progress/roadkill/utils.js"; +import { ChromeDriver } from "@progress/roadkill/chromedriver.js"; +import { Session, WebDriverClient, type Element as WebDriverElement } from "@progress/roadkill/webdriver.js"; +import { semantic, discover, SemanticObject, Root, findElementsByCss, FindByCSSFields } from "@progress/roadkill/semantic.js"; +import { SemanticJSX, expectSemanticMatch } from "@progress/roadkill/semantic-jsx.js"; + +const enableLogging = false; + +@semantic() +class LoginPage extends SemanticObject { + titleText?: string | null; + userInput?: WebDriverElement | null; + passInput?: WebDriverElement | null; + submitBtn?: WebDriverElement | null; + + static find() { + return findElementsByCss>( + "main.page-login", + element => ({ + titleText: (element.querySelector("h1")?.textContent || "").trim(), + userInput: element.querySelector("#username"), + passInput: element.querySelector("#password"), + submitBtn: element.querySelector("button[type=submit]"), + })); + } + + async login({ username, password }: { username: string; password: string }) { + await this.userInput!.clear(); + await this.userInput!.sendKeys(username); + await this.passInput!.clear(); + await this.passInput!.sendKeys(password); + await this.submitBtn!.click(); + } +} + +@semantic() +class GdprFrame extends SemanticObject { + static find() { + return findElementsByCss>("iframe.overlay-frame", () => ({})); + } + + async switchToFrame() { + await this.element!.switchToFrame(); + } +} + +@semantic() +class GdprPanel extends SemanticObject { + headerText?: string; + acceptBtn?: WebDriverElement | null; + + static find() { + return findElementsByCss>( + "main.page-gdpr", + element => ({ + headerText: (element.querySelector("h2")?.textContent || "").trim(), + acceptBtn: element.querySelector("#accept"), + }) + ); + } + + async accept() { + await this.acceptBtn!.click(); + } +} + +@semantic() +class TocPage extends SemanticObject { + titleText?: string; + subtitleText?: string; + cardCount?: number; + + static find() { + return findElementsByCss>( + "main.page-topics", + element => ({ + titleText: (element.querySelector("h1")?.textContent || "").trim(), + subtitleText: (element.querySelector(".muted")?.textContent || "").trim(), + cardCount: element.querySelectorAll("#topics-grid .card").length, + }) + ); + } + + cards(): TopicCard[] { return this.childrenOfType(TopicCard); } +} + +@semantic() +class TopicCard extends SemanticObject { + title?: string; + description?: string; + href?: string; + + static find() { + return findElementsByCss>( + "#topics-grid .card", + element => ({ + title: (element.querySelector("h3")?.textContent || "").trim(), + description: (element.querySelector("p.muted")?.textContent || "").trim(), + href: (element.querySelector("a[href]") as HTMLAnchorElement | null)?.href || "", + }) + ); + } +} + +describe("test-website (semantic objects)", () => { + let chromedriver: ChromeDriver; + let express: Express; + let webdriver: WebDriverClient; + let session: Session; + + beforeAll(async () => { + const { signal } = getState(); + + express = new Express( + Express.npmStart({ cwd: dirname(fileURLToPath(import.meta.url)), enableLogging }), + ); + await express.start(signal); + + chromedriver = new ChromeDriver({ args: ["--port=5033"], enableLogging }); + await chromedriver.start(signal); + + webdriver = new WebDriverClient({ address: chromedriver.address!, enableLogging }); + session = await webdriver.newSession({ + capabilities: { timeouts: { implicit: 2000 } }, + }); + }, 60000); + + beforeEach(async () => { + await session.setTimeouts({ implicit: 2000 }); + }); + + afterEach(async () => { + const { test } = getState(); + if (test && test.status === "fail") { + const screenshot = await session.takeScreenshot(); + const dir = `dist/test/${expect.getState().currentTestName}-semantic`; + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, "screenshot.png"), screenshot, { encoding: "base64" }); + } + }); + + afterAll(async () => { + await session?.dispose(); + await chromedriver?.dispose(); + await express?.dispose(); + }, 20000); + + test.only("login + topics using semantic objects", async () => { + + await step("navigate to local test site", () => + session.navigateTo(express.address!) + ); + + let root: Root = await step("discover login page + iframe", async () => { + const r = await discover(session); + + expectSemanticMatch(r, + + + + + ); + + expect(r.toXML(" ")).toBe( + ` + + + +` + ); + return r; + }); + + await step("switch to GDPR iframe", async () => { + const login = root.childrenOfType(LoginPage)[0]; + const frame = login.childrenOfType(GdprFrame)[0]; + expect(frame).toBeDefined(); + await frame.switchToFrame(); + }); + + await step("discover & accept GDPR", async () => { + const r = await discover(session); + + expectSemanticMatch(r, + + + ); + + expect(r.toXML(" ")).toBe( + ` + +` + ); + const panel = r.childrenOfType(GdprPanel)[0]; + await panel.accept(); + }); + + await step("switch back & perform login", async () => { + await session.switchToFrame(null); + const r = await discover(session); + const login = r.childrenOfType(LoginPage)[0]; + await login.login({ username: "admin", password: "1234" }); + }); + + await step("wait for redirect to /toc", async () => { + const deadline = Date.now() + 12_000; + while (Date.now() < deadline) { + if ((await session.getCurrentUrl()).includes("/toc")) return; + await new Promise(r => setTimeout(r, 50)); + } + throw new Error("Timed out waiting for /toc"); + }); + + await step("discover topics & verify snapshot", async () => { + const r = await discover(session); + + expectSemanticMatch(r, + + + + + + + + + ); + + const xml = r.toXML(" "); + expect(xml).toBe( + ` + + + + + + + +` + ); + }); + + await step("screenshot topics page", async () => { + const screenshot = await session.takeScreenshot(); + const dir = `dist/test/${expect.getState().currentTestName}-semantic`; + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, "screenshot.png"), screenshot, { encoding: "base64" }); + }); + }, 30000); +}); diff --git a/examples/test-website/index.test.ts b/examples/test-website/index.test.ts new file mode 100644 index 0000000..4f628c3 --- /dev/null +++ b/examples/test-website/index.test.ts @@ -0,0 +1,114 @@ +import { ChromeDriver } from "@progress/roadkill/chromedriver.js"; +import { Express } from "@progress/roadkill/express.js"; +import { Session, WebDriverClient, by } from "@progress/roadkill/webdriver.js"; +import { describe, test, expect, beforeAll, afterAll, afterEach, beforeEach } from "@jest/globals"; +import { getState, step } from "@progress/roadkill/utils.js"; +import { mkdir, writeFile } from "fs/promises"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const enableLogging = false; + +describe("test-website", () => { + let chromedriver: ChromeDriver; + let express: Express; + let webdriver: WebDriverClient; + let session: Session; + + beforeAll(async () => { + const { signal } = getState(); + + express = new Express( + Express.npmStart({ cwd: dirname(fileURLToPath(import.meta.url)), enableLogging }), + ); + await express.start(signal); + + chromedriver = new ChromeDriver({ args: ["--port=5032"], enableLogging }); + await chromedriver.start(signal); + + webdriver = new WebDriverClient({ address: chromedriver.address!, enableLogging }); + session = await webdriver.newSession({ + capabilities: { timeouts: { implicit: 2000 } }, + }); + }, 60000); + + // Before each: set implicit: 2000 + beforeEach(async () => { + await session.setTimeouts({ implicit: 2000 }); + }); + + afterEach(async () => { + const { test } = getState(); + if (test && test.status === "fail") { + console.log("collecting failure artifacts for:", test.names.join(" > ")); + const screenshot = await session.takeScreenshot(); + const dir = `dist/test/${expect.getState().currentTestName}`; + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, "screenshot.png"), screenshot, { encoding: "base64" }); + } + }); + + afterAll(async () => { + await session?.dispose(); + await chromedriver?.dispose(); + await express?.dispose(); + }, 20000); + + test("login flow", async () => { + const { signal } = getState(); + + await step("navigate to local test site", () => + session.navigateTo(express.address!) + ); + + await step("accept GDPR overlay", async () => { + const iframe = await session.findElement(by.css(".overlay-frame")); + await iframe.switchToFrame(); + + const accept = await session.findElement(by.css("button:nth-of-type(1)")); + await accept.click(); + + // Back to main/top-level browsing context + await session.switchToFrame(null); + }); + + await step("perform login", async () => { + const user = await session.findElement(by.css("#username")); + await user.clear(); + await user.sendKeys("admin"); + + const pass = await session.findElement(by.css("#password")); + await pass.clear(); + await pass.sendKeys("1234"); + + const btn = await session.findElement(by.css("button[type=submit]")); + await btn.click(); + }); + + // Assert navigation and page-unique content + await step("wait for Topics page", async () => { + await session.setTimeouts({ implicit: 10000 }); + + const grid = await session.findElement(by.css("#topics-grid")); + expect(await grid.getTagName()).toBe("div"); + + const url = await session.getCurrentUrl(); + expect(url).toContain("/toc"); + + await session.setTimeouts({ implicit: 2000 }); + }); + + await step("verify cards exist on Topics page", async () => { + const links = await session.findElements(by.css("#topics-grid .card a[href]")); + expect(links.length).toBeGreaterThanOrEqual(3); + }); + + await step("capture and save screenshot", async () => { + const screenshot = await session.takeScreenshot(); + const dir = `dist/test/${expect.getState().currentTestName}`; + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, "screenshot.png"), screenshot, { encoding: "base64", signal }); + }); + + }, 30000); +}); diff --git a/examples/test-website/index.ts b/examples/test-website/index.ts new file mode 100644 index 0000000..d70c168 --- /dev/null +++ b/examples/test-website/index.ts @@ -0,0 +1,287 @@ +// index.ts +// +// Localhost-only demo site for Roadkill WebDriver QA. +// - Guards all routes to localhost +// - Login page with GDPR overlay in an + + + + +`; + res.type("html").send(html("Roadkill – Login", body)); +}); + +// GDPR iframe content (light UI, inline errors) +app.get("/gdpr", (_req: Request, res: Response) => { + const body = ` +
+
+
+

GDPR Consent

+

+ This demo stores a single cookie gdprAccepted to enable the login form. + No other data is collected. +

+
+
+ + +

+
+
+
+
+ +`; + res.type("html").send(html("GDPR Consent", body)); +}); + +// Accept GDPR -> set cookie +app.post("/accept-gdpr", (_req: Request, res: Response) => { + res.cookie("gdprAccepted", "true", { + httpOnly: false, + sameSite: "strict", + secure: false, + maxAge: 24 * 60 * 60 * 1000 + }); + res.json({ ok: true }); +}); + +// Login endpoint – verify credentials, return randomized delay +app.post("/login", (req: Request, res: Response) => { + const { username, password } = (req.body ?? {}) as { username?: string; password?: string }; + if (username === "admin" && password === "1234") { + const delayMs = Math.floor(500 + Math.random() * 1000); // 500–1500 + return res.json({ ok: true, delayMs }); + } + return res.status(401).json({ ok: false, error: "Invalid credentials" }); +}); + +// Topics page (cards) +app.get("/toc", (_req: Request, res: Response) => { + const topics = [ + { title: "ChromeDriver", desc: "Standalone server implementing the WebDriver protocol for Chromium browsers. Roadkill manages lifecycle, logs, and startup detection.", href: "https://chromedriver.chromium.org/" }, + { title: "WebDriver", desc: "The W3C-standard browser automation protocol. Roadkill stays close to spec with typed commands and helpful errors.", href: "https://www.w3.org/TR/webdriver2/" }, + { title: "Semantic Objects", desc: "Higher-level DOM discovery helpers that make selectors readable, robust, and LLM-friendly.", href: "#semantic-objects" }, + { title: "Roadkill CLI", desc: "Checks Chrome/Node/ChromeDriver versions, manages drivers, and streamlines CI/dev workflows.", href: "#roadkill-cli" }, + { title: "MCP Integration", desc: "Expose Roadkill via the Model Context Protocol so LLMs can inspect pages and iteratively author tests.", href: "https://modelcontextprotocol.io/" } + ]; + + const cards = topics.map(t => ` +
+

${t.title}

+

${t.desc}

+

Learn more

+
+ `).join(""); + + const body = ` +
+
+
+

Roadkill – Topics

+

Targetable summary cards for QA flows.

+
+
+
${cards}
+
+

⬅ Back to Login

+
+
+`; + res.type("html").send(html("Roadkill – Topics", body)); +}); + +// Health check +app.get("/healthz", (_req, res) => res.json({ ok: true })); + +// ---- Boot -------------------------------------------------------------------- +if (import.meta.url === `file://${process.argv[1]}`) { + const port = Number(process.env.PORT || 3000); + app.listen(port, () => { + console.log(`Test site running at http://localhost:${port}`); + }); +} + +export default app; diff --git a/examples/test-website/package.json b/examples/test-website/package.json new file mode 100644 index 0000000..40280a3 --- /dev/null +++ b/examples/test-website/package.json @@ -0,0 +1,30 @@ +{ + "name": "test-website", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node --experimental-strip-types --no-warnings index.ts", + "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit --no-warnings" + }, + "dependencies": { + "cookie-parser": "^1.4.6", + "express": "^4.19.2" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.9", + "@types/express": "^4.17.23", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "@jest/globals": "^29.7.0", + "@progress/roadkill": "^0.2.4", + "@types/jest": "^29.5.5", + "@types/node": "^20.8.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "jest": { + "preset": "ts-jest/presets/default-esm", + "testEnvironment": "@progress/roadkill/jest-environment.ts" + } +} diff --git a/examples/test-website/tsconfig.json b/examples/test-website/tsconfig.json new file mode 100644 index 0000000..efc4754 --- /dev/null +++ b/examples/test-website/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true, + "noEmit": true, + "experimentalDecorators": true, + + "jsx": "react", + "jsxFactory": "SemanticJSX.createElement", + + "disableSourceOfProjectReferenceRedirect": true, + "preserveSymlinks": true, + }, + "include": ["index.ts", "index.test.ts", "index.semantic.test.tsx"], + "exclude": ["node_modules/**"] +} \ No newline at end of file diff --git a/lerna.json b/lerna.json index 37231ff..f2c5b3a 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,8 @@ "packages": [ "packages/@progress/roadkill", "examples/jest-web", - "examples/selenium-interop" + "examples/selenium-interop", + "examples/test-website" ], "command": { "bootstrap": { diff --git a/package-lock.json b/package-lock.json index b466bf5..d55007d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "workspaces": [ "packages/@progress/roadkill", "examples/jest-web", - "examples/selenium-interop" + "examples/selenium-interop", + "examples/test-website" ], "devDependencies": { "lerna": "^7.3.0" }, "engines": { - "node": "^18.17.0", - "npm": "^9.9.0" + "node": "^24.1.0", + "npm": "^11.3.0" } }, "examples/jest-web": { @@ -43,6 +44,301 @@ "ts-jest": "^29.1.1" } }, + "examples/test-website": { + "version": "0.1.0", + "dependencies": { + "cookie-parser": "^1.4.6", + "express": "^4.19.2" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@progress/roadkill": "^0.2.4", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^4.17.23", + "@types/jest": "^29.5.5", + "@types/node": "^20.8.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "examples/test-website/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "examples/test-website/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "examples/test-website/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "examples/test-website/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "examples/test-website/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "examples/test-website/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "examples/test-website/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "examples/test-website/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "examples/test-website/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "examples/test-website/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "examples/test-website/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "examples/test-website/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "examples/test-website/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "examples/test-website/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "examples/test-website/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "examples/test-website/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "examples/test-website/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "examples/test-website/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "examples/test-website/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "examples/test-website/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -693,6 +989,30 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", @@ -1274,6 +1594,29 @@ "node": "^14.17.0 || >=16.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.0.tgz", + "integrity": "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1928,6 +2271,34 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tufjs/canonical-json": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", @@ -2015,6 +2386,37 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/decompress": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", @@ -2023,6 +2425,32 @@ "@types/node": "*" } }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", @@ -2032,6 +2460,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2066,6 +2501,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -2099,6 +2541,20 @@ "xmlbuilder": ">=11.0.1" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/selenium-webdriver": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.18.tgz", @@ -2108,6 +2564,29 @@ "@types/ws": "*" } }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -2205,6 +2684,75 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -2248,6 +2796,22 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -2326,6 +2890,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2341,6 +2912,12 @@ "node": ">=8" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -2546,6 +3123,38 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2695,6 +3304,15 @@ "node": ">=12.17" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -2794,6 +3412,35 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3133,6 +3780,27 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/conventional-changelog-angular": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", @@ -3255,11 +3923,61 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -3307,11 +4025,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3325,7 +4050,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3355,12 +4079,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3616,12 +4340,31 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-indent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", @@ -3640,6 +4383,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3694,6 +4447,20 @@ "node": ">=12" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -3706,6 +4473,12 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", @@ -3744,6 +4517,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -3823,6 +4605,36 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3831,6 +4643,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3853,12 +4671,42 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", @@ -3913,6 +4761,84 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", "dev": true }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3939,6 +4865,12 @@ "node": ">=0.6.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3958,8 +4890,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fastq": { "version": "1.15.0", @@ -4052,6 +4983,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4136,6 +5084,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4196,6 +5162,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -4232,6 +5207,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4271,6 +5270,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", @@ -4439,6 +5451,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4492,12 +5516,36 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -4522,6 +5570,31 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -4571,7 +5644,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -4831,6 +5903,15 @@ "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", "dev": true }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4970,6 +6051,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-ssh": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", @@ -5032,8 +6119,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -5800,6 +6886,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -6357,6 +7449,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -6471,6 +7581,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6486,6 +7608,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -6499,11 +7630,22 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6512,7 +7654,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -6772,11 +7913,11 @@ "node": ">=0.10.0" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multimatch": { "version": "5.0.0", @@ -6822,7 +7963,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7404,6 +8544,30 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7821,6 +8985,15 @@ "parse-path": "^7.0.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7843,7 +9016,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -7879,6 +9051,16 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7951,6 +9133,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8066,12 +9257,34 @@ "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "dev": true }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", @@ -8088,6 +9301,21 @@ } ] }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8117,6 +9345,46 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -8661,6 +9929,22 @@ "node": ">=8" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -8706,7 +9990,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -8725,8 +10008,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/seek-bzip": { "version": "1.0.6", @@ -8773,6 +10055,64 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8785,6 +10125,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -8801,7 +10147,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -8813,11 +10158,82 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9031,6 +10447,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9313,6 +10738,10 @@ "node": ">=8" } }, + "node_modules/test-website": { + "resolved": "examples/test-website", + "link": true + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -9426,6 +10855,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9436,7 +10874,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, "bin": { "tree-kill": "cli.js" } @@ -9502,6 +10939,50 @@ "node": ">=12" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -9566,6 +11047,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9573,10 +11089,11 @@ "dev": true }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9646,6 +11163,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -9686,11 +11212,29 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -9710,6 +11254,13 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", @@ -9746,6 +11297,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -10069,6 +11629,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10081,17 +11651,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "packages/@progress/roadkill": { "version": "0.2.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", "@types/decompress": "^4.2.5", "decompress": "^4.2.1", "plist": "^3.1.0", + "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { - "roadkill": "roadkill.js" + "roadkill": "roadkill.js", + "roadkill-mcp": "roadkill-mcp.js" }, "devDependencies": { "@types/jest": "^29.5.6", @@ -10099,7 +11690,6 @@ "@types/plist": "^3.0.3", "@types/yargs": "^17.0.25", "jest": "^29.7.0", - "tree-kill": "^1.2.2", "typescript": "^5.2.2" } }, diff --git a/package.json b/package.json index 0bf0dc6..05caf6d 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "workspaces": [ "packages/@progress/roadkill", "examples/jest-web", - "examples/selenium-interop" + "examples/selenium-interop", + "examples/test-website" ], "engines": { - "node": "^18.17.0", - "npm": "^9.9.0" + "node": "^24.1.0", + "npm": "^11.3.0" }, "private": true, "repository": { diff --git a/packages/@progress/roadkill/.mcp/server.json b/packages/@progress/roadkill/.mcp/server.json new file mode 100644 index 0000000..16bf632 --- /dev/null +++ b/packages/@progress/roadkill/.mcp/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json", + "name": "io.github.telerik/roadkill", + "description": "Roadkill MCP: WebDriver utilities exposed to LLMs over stdio.", + "packages": [ + { + "registry_name": "npm", + "name": "@progress/roadkill", + "version": "0.2.4", + "package_arguments": [], + "environment_variables": [] + } + ], + "version_detail": { + "version": "0.2.4" + }, + "repository": { + "url": "https://github.com/telerik/roadkill", + "source": "github" + } +} diff --git a/packages/@progress/roadkill/SEMANTIC.md b/packages/@progress/roadkill/SEMANTIC.md new file mode 100644 index 0000000..62b5de7 --- /dev/null +++ b/packages/@progress/roadkill/SEMANTIC.md @@ -0,0 +1,173 @@ +# Semantic Objects + +**Semantic Objects** extend Page Objects with *discovery* and *snapshots*: + +* Discovered in the **browser** via a pure `static find()` (one round-trip). +* Hydrated into classes with **readonly properties** (captured by `find()`). +* Arranged in a **tree** by **DOM containment** using each object’s anchor **element**. +* Used with **methods for interactions** (click/type) and **properties for assertions**. + +The workflow is: **discover → hydrate → assert → (optionally) interact**. + +--- + +## Why not just Page Objects? + +Classic Page Objects are predefined and often read from the DOM during tests, causing chatty round-trips and drift. +Semantic Objects are **discovered at runtime**, capturing values up front so assertions are **synchronous** and **stable**. + +--- + +## Design + +* Every semantic class: + + * uses `@semantic()` and implements a **pure** `static find()` (no imports/closures). + * returns **DTOs**: `{ element, ...props }[]` (site-specific selectors). + * has a single constructor `(driver, dto)` that hydrates **readonly fields** from the DTO. + * exposes **methods** only for interactions (async). + +* The runner sends all `find()` functions in a single `executeScript`, builds the **tree in the browser** using `element.contains(...)`, and returns only root DTOs. + +* Node hydrates DTOs to instances and you get: + + * `JSON.stringify(root, null, " ")` → compact tuple snapshots: `["Class", {props}, ...children]` + * `root.toXML()` → pretty XML snapshots (future XPath-ready) + +--- + +## Quick start + +```ts +// discovery +const root = await discover(driver); + +// assert via hydrated properties (no extra reads) +const items = root.children.filter(x => x instanceof ShoppingItem) as ShoppingItem[]; +expect(items[0].name).toBe("USB-C Charger"); + +// interact only when needed +await items[0].addToCart(); + +// snapshots +console.log(JSON.stringify(root, null, " ")); +console.log(root.toXML()); +``` + +See `example.ts` for `ShoppingPage`, `Navigation`, `NavigationLink`, `ShoppingItem`. + +--- + +## Authoring a semantic object + +```ts +@semantic() +export class ShoppingItem extends SemanticObject { + public readonly name: string; + public readonly priceText: string; + private readonly addButton: WebElement; + + static find(): DTO<{ name: string; priceText: string; addButton: Element }>[] { + return findElementsByCss(".item-card", card => ({ + element: card, + name: (card.querySelector(".item-title")!.textContent || "").trim(), + priceText: (card.querySelector(".item-price")!.textContent || "").trim(), + addButton: card.querySelector(".add-to-cart")! + })); + } + + constructor(driver: WebDriver, dto: DTOOf) { + super(driver, dto); + this.name = dto.name; + this.priceText = dto.priceText; + this.addButton = dto.addButton; + } + + async addToCart() { await this.addButton.click(); } +} +``` + +### Recommendations + +* **Read in `find()`**: capture all values you’ll assert (strings/numbers/booleans). +* **Be strict**: if a field/button is required, ensure it in `find()` (skip/throw). +* **Single-site selectors**: prefer one clear selector per property; avoid multi-site heuristics. +* **One element per object**: the `element` is the anchor; other handles (e.g., `addButton`) can be captured as fields. +* **Methods for actions only**: keep assertions on readonly properties; actions are the only async part. + +--- + +## Built-in helper (usable inside `find()`) + +### `findElementsByCss(selector, mapFn)` + +Injected into the browser so that any `find()` can use it directly. + +* **selector**: CSS selector string +* **mapFn**: `(element: Element) => { element, …props }` +* **returns**: DTO array + +Example: + +```ts +static find(): DTO<{ text: string }>[] { + return findElementsByCss("nav.site-nav a", el => ({ + element: el, + text: (el.textContent || "").trim() + })); +} +``` + +--- + +## Snapshots + +* **JSON tuples** (machine-friendly, easy diffs): + + ```json + [ + "ShoppingItem", + { "name": "Item A", "priceText": "$19.99" } + ] + ``` +* **XML** (human-friendly, compact): + + ```xml + + ``` + +Use either/both in golden snapshot tests. XML also sets you up for XPath later if you choose to mirror to a DOM and map back to semantic instances. + +--- + +## API (short) + +```ts +export function semantic(): ClassDecorator; +export async function discover(driver: WebDriver): Promise; + +export class SemanticObject { + readonly element: WebElement | null; + readonly children: SemanticObject[]; + childrenOfType(ctor: new (...args:any[]) => T): T[]; + toJSON(): any; // ["Class",{props},...children] + toXML(): string; // ... +} + +export class Root extends SemanticObject {} + +export type DTO = { element: Element } & T; +export type DTOOf any }> = AnnotatedDTO>>; +``` + +--- + +## Notes + +* `find()` must be **pure** (serializable) because it’s injected via `.toString()` and executed in the browser sandbox. +* The tree is built **in the browser** to avoid many `executeScript` calls on Node. +* If your app state changes (modal opens, list expands), call `discover(driver)` again to hydrate a fresh tree. + +--- + +Happy testing! 🎯 diff --git a/packages/@progress/roadkill/chromedriver.ts b/packages/@progress/roadkill/chromedriver.ts index 8c02a12..5cbf75a 100644 --- a/packages/@progress/roadkill/chromedriver.ts +++ b/packages/@progress/roadkill/chromedriver.ts @@ -73,20 +73,26 @@ export class ChromeDriver extends Server { protected override onLine(line: string): void { super.onLine(line); - if (this.state == "starting") { - if (this._port == undefined) { - const result = /Starting ChromeDriver.*on port (\d*)/.exec(line); - if (result) this._port = Number.parseInt(result[1]); - } + if (this.state !== "starting") return; - if (line == "ChromeDriver was started successfully.") { - this._address = `http://localhost:${this._port}`; - this.state = "running"; - this.started(); - } + const l = line.trim(); + + // Capture port if present anywhere in the line + if (this._port === undefined) { + const m = /on port\s+(\d+)/i.exec(l); + if (m) this._port = Number.parseInt(m[1], 10); + } - this.startupLine.push(line); + // Transition to running when success message appears (with or without "on port ...") + if (/ChromeDriver was started successfully/i.test(l)) { + if (!this._address) { + const port = this._port ?? 9515; // fall back to known port if not parsed yet + this._address = `http://localhost:${port}`; + } + this.started(); // <-- this sets state = "running" when in "starting" } + + this.startupLine.push(l); } protected override startingErrorOnClose(code: number): Error { diff --git a/packages/@progress/roadkill/express.ts b/packages/@progress/roadkill/express.ts new file mode 100644 index 0000000..87a533f --- /dev/null +++ b/packages/@progress/roadkill/express.ts @@ -0,0 +1,128 @@ +import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "child_process"; +import { delimiter } from "node:path"; +import { Server, type ServerOptions } from "./server.js"; + +export interface ExpressOptions extends ServerOptions { + /** Working directory for the process. Defaults to process.cwd(). */ + cwd?: string; + + /** Command to start the server (e.g., "npm", "node"). */ + command: string; + + /** Arguments for the command. */ + args: string[]; + + /** Additional environment variables for the process. */ + env?: NodeJS.ProcessEnv; + + /** Additional PATH entries (prepended). */ + pathPrepend?: string[]; + + /** Fallback port if no address is parsed. Defaults to 3000. */ + defaultPort?: number; + + /** Regex to detect readiness in stdout. */ + readinessRegex?: RegExp; + + /** Regex to extract the address from stdout. */ + addressRegex?: RegExp; + + /** Entry file for node-ts helper. Defaults to "index.ts". */ + entry?: string; +} + +export class Express extends Server { + private _address?: string; + private startupLines: string[] = []; + + public get address(): string | undefined { return this._address; } + public override get prefix() { return this.options?.logPrefix ?? "Express"; } + + protected spawn(): ChildProcessWithoutNullStreams { + const { + cwd = process.cwd(), + env, + pathPrepend = [], + defaultPort = 3000, + command, + args, + } = this.options; + + const childEnv: NodeJS.ProcessEnv = { ...process.env, ...env }; + if (pathPrepend.length) { + const currentPath = process.env.PATH ?? ""; + childEnv.PATH = `${pathPrepend.join(delimiter)}${delimiter}${currentPath}`; + } + + const spawnOpts: SpawnOptionsWithoutStdio = { + cwd, + env: childEnv, + shell: true, + }; + + this._address = `http://localhost:${defaultPort}`; + return spawn(command, args, spawnOpts); + } + + protected override onLine(line: string): void { + super.onLine(line); + const l = (line ?? "").trim(); + this.startupLines.push(l); + + if (this.state !== "starting") return; + + const readiness = this.options?.readinessRegex ?? /Test site running at (https?:\/\/localhost:\d+)/i; + const addressRegex = this.options?.addressRegex ?? /(https?:\/\/localhost:\d+)/i; + + if (readiness.test(l)) { + const m = addressRegex.exec(l); + if (m && m[1]) this._address = m[1]; + this.started(); + return; + } + + const m2 = addressRegex.exec(l); + if (m2 && m2[1]) { + this._address = m2[1]; + this.started(); + } + } + + protected override startingErrorOnClose(code: number): Error { + if (this.startupLines.length) { + return new Error( + `Express server failed to start. Code ${code}.\n\n${this.startupLines.join("\n")}` + ); + } else { + return new Error(`Express server failed to start. Code ${code}.`); + } + } + + // Helpers to build options + public static nodeTs(entry = "index.ts", opts: Partial = {}): ExpressOptions { + return { + command: "node", + args: ["--experimental-strip-types", "--no-warnings", entry], + defaultPort: 3000, + ...opts, + }; + } + + public static npmStart(opts: Partial = {}): ExpressOptions { + return { + command: "npm", + args: ["start", "--silent"], + defaultPort: 3000, + ...opts, + }; + } + + public static custom(command: string, args: string[], opts: Partial = {}): ExpressOptions { + return { + command, + args, + defaultPort: 3000, + ...opts, + }; + } +} diff --git a/packages/@progress/roadkill/jest-environment.ts b/packages/@progress/roadkill/jest-environment.ts index 4e066ea..de211df 100644 --- a/packages/@progress/roadkill/jest-environment.ts +++ b/packages/@progress/roadkill/jest-environment.ts @@ -63,15 +63,16 @@ const red = color ? (text: string) => `\x1b[31m${text}\x1b[0m` : (text: string) abstract class Scope { static scopes: Scope[] = []; - static consoleLog; - static scopeLog = function () { + static consoleLog?: (...args: unknown[]) => void; + static scopeLog = function (...args: unknown[]) { Scope.flush(); - consoleModule.log(...arguments); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (consoleModule.log as any)(...args); }; - static flush(to: Scope = undefined) { + static flush(to?: Scope) { if (Scope.consoleLog != undefined) { - consoleModule.log = Scope.consoleLog; + consoleModule.log = Scope.consoleLog as any; Scope.consoleLog = undefined; } @@ -81,9 +82,10 @@ abstract class Scope { consoleModule.group(`${scope}`); } - if (to == scope) { - Scope.consoleLog = consoleModule.log; - consoleModule.log = Scope.scopeLog; + if (to === scope) { + Scope.consoleLog = consoleModule.log as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + consoleModule.log = Scope.scopeLog as any; break; } } @@ -93,11 +95,11 @@ abstract class Scope { protected endPrinted = false; constructor( - protected readonly parent: Scope, + protected readonly parent: Scope | undefined, protected readonly name: string) { } - static get top(): Scope { + static get top(): Scope | undefined { if (this.scopes.length == 0) { return undefined; } else { @@ -105,17 +107,22 @@ abstract class Scope { } } - static pop(): Scope { - Scope.top.end(); - return Scope.scopes.pop(); + static pop(): Scope | undefined { + const top = Scope.top; + if (top) { + top.end(); + return Scope.scopes.pop(); + } + return undefined; } begin() { Scope.scopes.push(this); if (consoleModule.log != Scope.scopeLog) { - Scope.consoleLog = consoleModule.log; - consoleModule.log = Scope.scopeLog; + Scope.consoleLog = consoleModule.log as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + consoleModule.log = Scope.scopeLog as any; } } @@ -168,12 +175,12 @@ export interface HookState { class FunctionScope extends Scope { public readonly names: ReadonlyArray; - public readonly source: string; + public readonly source?: string; private sourcePrinted = false; - private startTime: number; + private startTime = 0; - constructor(parent: Scope, name: string, source: string, names: string[]) { + constructor(parent: Scope | undefined, name: string, source: string | undefined, names: string[]) { super(parent, name); this.source = source; this.names = names; @@ -187,9 +194,10 @@ class FunctionScope extends Scope { protected formatSource(): string { if (this.sourcePrinted) return ""; this.sourcePrinted = true; + if (!this.source) return ""; try { const base = process.env.INIT_CWD; - return gray(", at " + relative(base, this.source)); + return gray(", at " + relative(base ?? "", this.source)); } catch { return gray(", at " + this.source); } @@ -202,16 +210,16 @@ class FunctionScope extends Scope { class HookScope extends FunctionScope implements HookState { - private _error: Error; + private _error?: Error; private _status: "started" | "fail" | "pass" = "started"; private conclusionPrinted = false; private beginWithDotDotDot = false; - private beginTimeout: NodeJS.Timeout; - private onTimeout: () => void; + private beginTimeout?: NodeJS.Timeout; + private onTimeout?: () => void; public get status() { return this._status; } - public get error() { return this.error; } + public get error() { return this._error; } public get fullName() { return this.names.join(" "); } public get hookName() { return this.name; } @@ -226,7 +234,7 @@ class HookScope extends FunctionScope implements HookState { this.beginTimeout = setTimeout(this.onTimeout, 10000); } - fail(error: Error = undefined) { + fail(error?: Error) { this._status = "fail"; this._error = error; if (this._error) { @@ -273,16 +281,16 @@ export interface TestState { class TestScope extends FunctionScope implements TestState { - private _error: Error; + private _error?: Error; private _status: "started" | "fail" | "pass" = "started"; private conclusionPrinted = false; private beginWithDotDotDot = false; - private beginTimeout: NodeJS.Timeout; - private onTimeout: () => void; + private beginTimeout?: NodeJS.Timeout; + private onTimeout?: () => void; public get status() { return this._status; } - public get error() { return this.error; } + public get error() { return this._error; } public get fullName() { return this.names.join(" "); } public get testName() { return this.names[this.names.length - 1]; } @@ -297,7 +305,7 @@ class TestScope extends FunctionScope implements TestState { this.beginTimeout = setTimeout(this.onTimeout, 10000); } - fail(error: Error = undefined) { + fail(error?: Error) { this._status = "fail"; this._error = error; if (this._error) { @@ -337,18 +345,18 @@ class TestScope extends FunctionScope implements TestState { class TestSkip extends Scope implements TestState { public readonly names: ReadonlyArray; - private readonly source: string; + private readonly source?: string; private sourcePrinted = false; private _status: "skip" = "skip"; - constructor(parent: Scope, name: string, source: string, names: string[]) { + constructor(parent: Scope | undefined, name: string, source: string | undefined, names: string[]) { super(parent, name); this.names = names; this.source = source; } public get status() { return this._status; } - public get error() { return this.error; } + public get error() { return undefined; } public get fullName() { return this.names.join(" "); } public get testName() { return this.names[this.names.length - 1]; } @@ -356,12 +364,13 @@ class TestSkip extends Scope implements TestState { return `${gray("○")} ${this.name}${this.formatSource()}`; } - formatSource(): string { + private formatSource(): string { if (this.sourcePrinted) return ""; this.sourcePrinted = true; + if (!this.source) return ""; try { const base = process.env.INIT_CWD; - return gray(", at " + relative(base, this.source)); + return gray(", at " + relative(base ?? "", this.source)); } catch { return gray(", at " + this.source); } @@ -375,18 +384,19 @@ class TeardownScope extends Scope { } // Type checking is broken -const BaseEnvironment = (NodeEnvironment as any); +// (leave this exactly as-is per your request) +const BaseEnvironment = (((NodeEnvironment as any).default instanceof Function) ? (NodeEnvironment as any).default : NodeEnvironment) as any; class TestTimeout extends Error {} class HookTimeout extends Error {} class TestEnvironment extends BaseEnvironment { - constructor(config, context) { + constructor(config: any, context: any) { super(config, context); } - private static getNameStack(event) { + private static getNameStack(event: any) { let nameStack: string[] = []; @@ -432,13 +442,14 @@ class TestEnvironment extends BaseEnvironment { } } - private hookName(event): string { + private hookName(event: any): string { return event?.hook?.type; } - private source(event): string { + private source(event: any): string | undefined { try { - const hookStack = event?.hook?.asyncError?.stack || event?.test?.asyncError?.stack; + const hookStack: string | undefined = event?.hook?.asyncError?.stack || event?.test?.asyncError?.stack; + if (!hookStack) return undefined; const stackLines = hookStack.split("\n"); if (stackLines.length >= 2) { const line: string = stackLines[1]?.trim(); @@ -449,10 +460,10 @@ class TestEnvironment extends BaseEnvironment { let closeBrace = line.indexOf(")"); if (openBrace != -1 && closeBrace != -1 && openBrace < closeBrace) { - // at _dispatchDescribe (/Users/cankov/git/telerik/roadkill/node_modules/jest-circus/build/index.js:91:26) + // at _dispatchDescribe (/Users/.../node_modules/jest-circus/build/index.js:91:26) path = line.substring(openBrace + 1, closeBrace - 1); } else if (line.startsWith("at ")) { - // at /Users/cankov/git/telerik/roadkill/examples/jest-web/w3schools.test.ts:41:5 + // at /path/to/test.ts:41:5 path = line.substring(3); } else { // play safe @@ -466,15 +477,15 @@ class TestEnvironment extends BaseEnvironment { } } - async handleTestEvent(event, state) { + async handleTestEvent(event: any, state: any) { const nameStack = TestEnvironment.getNameStack(event); - if (event.name == "setup" && this.global.roadkillJestConsoleDefault !== false) { - this.global.console = consoleModule; + if (event.name == "setup" && (this.global as any).roadkillJestConsoleDefault !== false) { + (this.global as any).console = consoleModule; } - if (this.global.roadkillJestLifecycleLogging) { + if ((this.global as any).roadkillJestLifecycleLogging) { console.log(`[JEST] ${this.displayFriendlyEventName(event.name)}${event?.hook?.type ? " " + event?.hook?.type : ""}${nameStack.length ? " (" + nameStack.join(" > ") + ")" : ""}`); } @@ -484,22 +495,22 @@ class TestEnvironment extends BaseEnvironment { if (event.name == "setup") { new RootScope().begin(); - new SetupScope(Scope.top).begin(); + new SetupScope(Scope.top as RootScope).begin(); } else if (event.name == "run_start") { Scope.pop(); // Pops Setup - new RunScope(Scope.top).begin(); + new RunScope(Scope.top as RootScope).begin(); } else if (event.name == "run_describe_start") { - if (event.describeBlock.name == "ROOT_DESCRIBE_BLOCK") { + if (event.describeBlock?.name == "ROOT_DESCRIBE_BLOCK") { } else { - new DescribeScope(Scope.top, nameStack.join(" > ")).begin(); + new DescribeScope(Scope.top!, nameStack.join(" > ")).begin(); } } else if (event.name == "hook_start") { const hookScope = new HookScope( - Scope.top, + Scope.top!, this.hookName(event), this.source(event), nameStack); - this.global["@progress/roadkill/utils:hook"] = hookScope; + (this.global as any)["@progress/roadkill/utils:hook"] = hookScope; hookScope.begin(); } else if (event.name == "hook_success") { (Scope.top as HookScope).pass(); @@ -509,11 +520,11 @@ class TestEnvironment extends BaseEnvironment { Scope.pop(); } else if (event.name == "test_started") { const testScope = new TestScope( - Scope.top, + Scope.top!, nameStack.join(" > "), this.source(event), nameStack); - this.global["@progress/roadkill/utils:test"] = testScope; + (this.global as any)["@progress/roadkill/utils:test"] = testScope; testScope.begin(); } else if (event.name == "test_fn_failure") { const test = Scope.top as TestScope; @@ -523,18 +534,18 @@ class TestEnvironment extends BaseEnvironment { test.pass(); } else if (event.name == "test_done") { const test = (Scope.top as TestScope); - if (test.status == "started") { + if (test?.status == "started") { test.fail(); } Scope.pop(); } else if (event.name == "test_skip") { - const testScope = new TestSkip( - Scope.top, + new TestSkip( + Scope.top!, nameStack.join(" > "), this.source(event), nameStack).event(); } else if (event.name == "run_describe_finish") { - if (event.describeBlock.name == "ROOT_DESCRIBE_BLOCK") { + if (event.describeBlock?.name == "ROOT_DESCRIBE_BLOCK") { } else { Scope.pop(); } @@ -547,32 +558,34 @@ class TestEnvironment extends BaseEnvironment { switch (event.name) { case 'test_start': break; - case 'test_fn_start': - this.global["@progress/roadkill/utils:signal"] = undefined; - this.global.signal = undefined; - const testTimeout = (event?.test?.timeout ?? state?.testTimeout); + case 'test_fn_start': { + (this.global as any)["@progress/roadkill/utils:signal"] = undefined; + (this.global as any).signal = undefined; + const testTimeout = (event?.test?.timeout ?? state?.testTimeout) as number | undefined; if (testTimeout != undefined) { const controller = new AbortController(); - this.global["@progress/roadkill/utils:signal"] = controller.signal; - this.global.signal = controller.signal; + (this.global as any)["@progress/roadkill/utils:signal"] = controller.signal; + (this.global as any).signal = controller.signal; setTimeout(() => { controller.abort(new TestTimeout(`Exceeded timeout of ${testTimeout} ms for a test.`)); }, Math.max(0, testTimeout)); } break; - case 'hook_start': - this.global["@progress/roadkill/utils:signal"] = undefined; - this.global.signal = undefined; - const hookTimeout = (event?.hook?.timeout ?? state?.testTimeout); + } + case 'hook_start': { + (this.global as any)["@progress/roadkill/utils:signal"] = undefined; + (this.global as any).signal = undefined; + const hookTimeout = (event?.hook?.timeout ?? state?.testTimeout) as number | undefined; if (hookTimeout) { const controller = new AbortController(); - this.global["@progress/roadkill/utils:signal"] = controller.signal; - this.global.signal = controller.signal; + (this.global as any)["@progress/roadkill/utils:signal"] = controller.signal; + (this.global as any).signal = controller.signal; setTimeout(() => { controller.abort(new HookTimeout(`Exceeded timeout of ${hookTimeout} ms for a hook.`)); }, Math.max(0, hookTimeout)); } break; + } case 'test_done': break; } diff --git a/packages/@progress/roadkill/package.json b/packages/@progress/roadkill/package.json index 1ccd789..a13be54 100644 --- a/packages/@progress/roadkill/package.json +++ b/packages/@progress/roadkill/package.json @@ -7,7 +7,8 @@ "test": "node --experimental-vm-modules ../../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit" }, "bin": { - "roadkill": "./roadkill.js" + "roadkill": "./roadkill.js", + "roadkill-mcp": "./roadkill-mcp.js" }, "license": "MIT", "repository": { @@ -18,10 +19,12 @@ "access": "public" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", "@types/decompress": "^4.2.5", "decompress": "^4.2.1", "plist": "^3.1.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "tree-kill": "^1.2.2" }, "devDependencies": { "@types/jest": "^29.5.6", @@ -29,7 +32,6 @@ "@types/plist": "^3.0.3", "@types/yargs": "^17.0.25", "jest": "^29.7.0", - "tree-kill": "^1.2.2", "typescript": "^5.2.2" }, "files": [ diff --git a/packages/@progress/roadkill/roadkill-mcp.ts b/packages/@progress/roadkill/roadkill-mcp.ts new file mode 100644 index 0000000..fb9edae --- /dev/null +++ b/packages/@progress/roadkill/roadkill-mcp.ts @@ -0,0 +1,572 @@ +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +import { ChromeDriver } from "./chromedriver.js"; +import { WebDriverClient, Session, Element } from "./webdriver.js"; + +import { readFile } from "fs/promises"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +// ─────────────────────────────────────────────────────────── +// ChromeDriver singleton + runDriver() +// ─────────────────────────────────────────────────────────── + +const driver = new ChromeDriver({ + enableLogging: true, + log: console.error, + logPrefix: "ChromeDriver", + args: ["--port=9515", "--enable-chrome-logs"] +}); + +let driverWait: Promise | null = null; + +/** + * Idempotent bring-up for ChromeDriver: + * - running → resolve immediately with driver + * - new → start() once and wait + * - starting → wait for "running" or error + * - aborted/ disposed → throw + */ +async function runDriver(): Promise { + switch (driver.state) { + case "running": + return driver; + + case "new": + if (!driverWait) { + driverWait = driver + .start() + .then(() => driver) + .finally(() => { driverWait = null; }); + } + return driverWait; + + case "starting": + if (!driverWait) { + driverWait = new Promise((resolve, reject) => { + const onState = (s: string) => { + if (s === "running") { + cleanup(); + resolve(driver); + } else if (s === "disposed" || s === "abort start" || s === "abort running") { + cleanup(); + reject(new Error(`ChromeDriver failed to start (state: ${s}).`)); + } + }; + const cleanup = () => driver.off("state", onState as any); + driver.on("state", onState as any); + }).finally(() => { driverWait = null; }); + } + return driverWait; + + default: + throw new Error(`ChromeDriver is not available (state: ${driver.state}).`); + } +} + +// ─────────────────────────────────────────────────────────── +// Helpers +// ─────────────────────────────────────────────────────────── + +/** + * Return both a readable summary and a machine-parseable JSON payload. + * Using two text parts keeps compatibility with all MCP clients. + */ +function mcpResult(payload: T, summary?: string) { + const parts: Array<{ type: "text"; text: string }> = []; + if (summary) parts.push({ type: "text", text: summary }); + parts.push({ type: "text", text: JSON.stringify(payload) }); + return { content: parts }; +} + +// Resolve sibling file (prefers .ts, falls back to .js) +async function readSiblingModuleBase(nameNoExt: "webdriver" | "chromedriver"): Promise<{ path: string; content: string }> { + const base = dirname(fileURLToPath(import.meta.url)); + const tsPath = join(base, `${nameNoExt}.ts`); + const jsPath = join(base, `${nameNoExt}.js`); + try { + const content = await readFile(tsPath, "utf-8"); + return { path: tsPath, content }; + } catch { + try { + const content = await readFile(jsPath, "utf-8"); + return { path: jsPath, content }; + } catch { + throw new Error(`Could not find ${nameNoExt}.ts or ${nameNoExt}.js next to this MCP server.`); + } + } +} + +// ─────────────────────────────────────────────────────────── +// In-memory session store +// ─────────────────────────────────────────────────────────── +const sessions = new Map(); + +// ─────────────────────────────────────────────────────────── +// MCP server + tools +// ─────────────────────────────────────────────────────────── +const server = new McpServer({ + name: "roadkill-mcp", + version: "0.0.1", + description: + "Roadkill MCP: tools that let an LLM mimic WebDriver flows end-to-end. " + + "Typical usage: (1) webdriver.startSession → (2) webdriver.navigate → " + + "(3) webdriver.domSnapshot / webdriver.selectElements to explore and craft selectors → " + + "(4) webdriver.clickElement (and other future interactions) → (5) close session. " + + "Use the framework.* tools to read the shipped Roadkill WebDriver/ChromeDriver sources and fetch a Jest example. " + + "From user prompts, explore with the DOM tools, propose stable selectors (ids/roles/text), " + + "then generate portable tests in Jest using @progress/roadkill/webdriver.js." +}); + +// ── hello ────────────────────────────────────────────────── +server.tool( + "hello", + "Greets back the user! Useful for probing the MCP pipeline.", + { + name: z.string().describe("User name to greet") + }, + async ({ name }) => mcpResult({ hello: name }, `Hello, ${name}! 👋`) +); + +// ── webdriver.startSession ───────────────────────────────── +server.tool( + "webdriver.startSession", + "Start a WebDriver session and return a sessionId. " + + "LLM: Always call this first to obtain a session id before navigation or DOM exploration.", + { + browserName: z + .string() + .describe("Target browser name (e.g., 'chrome'). Defaults to 'chrome'.") + .default("chrome") + }, + async ({ browserName }) => { + const d = await runDriver(); + const address = String(d.address); + + const wd = new WebDriverClient({ + enableLogging: true, + // If your WebDriverClientOptions lacks `log`, keep the cast: + log: console.error as any, + address, + logPrefix: "[WebDriver]" + }); + + const session = await wd.newSession({ + capabilities: { browserName } + }); + + sessions.set(session.sessionId, session); + + const payload = { + sessionId: session.sessionId, + address, + capabilities: session.capabilities + }; + + return mcpResult( + payload, + `Started session ${session.sessionId} on ${address} (${session.capabilities.browserName} ${session.capabilities.browserVersion}).` + ); + } +); + +// ── webdriver.navigate ───────────────────────────────────── +server.tool( + "webdriver.navigate", + "Navigate an existing session to a URL. " + + "LLM: Use this immediately after starting a session and whenever you need a new page. Reuse the same sessionId.", + { + sessionId: z + .string() + .min(1) + .describe("Existing WebDriver session id"), + url: z + .string() + .url() + .describe("Absolute URL to navigate to (e.g., https://example.com)") + }, + async ({ sessionId, url }) => { + const session = sessions.get(sessionId); + if (!session) throw new Error(`Session not found: ${sessionId}`); + await session.navigateTo(url); + const current = await session.getCurrentUrl().catch(() => undefined); + return mcpResult( + { sessionId, url, currentUrl: current ?? url }, + `Navigated ${sessionId} → ${url}` + ); + } +); + +// ── webdriver.status ─────────────────────────────────────── +server.tool( + "webdriver.status", + "Report ChromeDriver state/address. " + + "LLM: Use this to diagnose driver availability if a session fails to start.", + {}, + async () => { + const state = driver.state; + const addr = driver.address ? String(driver.address) : null; + return mcpResult({ state, address: addr }, `ChromeDriver: ${state}${addr ? ` @ ${addr}` : ""}`); + } +); + +// ── webdriver.domSnapshot ────────────────────────────────── +server.tool( + "webdriver.domSnapshot", + "Return a trimmed DOM tree (tag, id, classes, role, aria-*, type, href, text). " + + "LLM: Use to understand page structure and propose stable selectors. Prefer id/role/unique text.", + { + sessionId: z + .string() + .min(1) + .describe("Existing WebDriver session id"), + rootSelector: z + .string() + .describe("Optional CSS selector for sub-tree root; defaults to document.body") + .optional(), + maxDepth: z + .number() + .describe("Max depth to traverse (default 64)") + .int() + .min(0) + .max(255) + .default(64), + maxChildren: z + .number() + .describe("Max direct children per node (default 64)") + .int() + .min(1) + .max(255) + .default(64), + maxTextLen: z + .number() + .describe("Max characters of normalized text captured per node (default 120)") + .int() + .min(0) + .max(2000) + .default(120) + }, + async ({ sessionId, rootSelector, maxDepth = 64, maxChildren = 64, maxTextLen = 120 }) => { + const session = sessions.get(sessionId); + if (!session) throw new Error(`Session not found: ${sessionId}`); + + const tree = await session.executeScript( + ` + return (function() { + const opts = arguments[0] || {}; + const selector = opts.rootSelector; + const maxDepth = Number.isFinite(opts.maxDepth) ? opts.maxDepth : 64; + const maxChildren = Number.isFinite(opts.maxChildren) ? opts.maxChildren : 64; + const maxTextLen = Number.isFinite(opts.maxTextLen) ? opts.maxTextLen : 120; + + const root = selector ? document.querySelector(selector) : document.body; + if (!root) return { error: "root-not-found", selector }; + + const ELEMENT_NODE = 1; + const norm = s => (s ?? "").replace(/\\s+/g, " ").trim(); + + function nodeTypeTag(el) { return el.tagName ? el.tagName.toLowerCase() : ""; } + function nodeTypeAttr(el, tag) { + if (tag === "button" && el.type) return String(el.type); + if (tag === "input" && el.type) return String(el.type); + return undefined; + } + function nodeHref(el, tag) { return (tag === "a" && el.href) ? String(el.href) : undefined; } + function nodeText(el) { + const raw = (el.innerText ?? el.textContent ?? ""); + const txt = norm(raw); + return maxTextLen > 0 ? txt.slice(0, maxTextLen) : txt; + } + + function snap(el, depth) { + if (!el || (el.nodeType || 0) !== ELEMENT_NODE) return null; + if (depth > maxDepth) return null; + + const tag = nodeTypeTag(el); + const id = el.id || undefined; + const classes = el.classList ? Array.from(el.classList) : []; + const role = el.getAttribute && el.getAttribute("role") || undefined; + const type = nodeTypeAttr(el, tag); + const href = nodeHref(el, tag); + const text = nodeText(el); + + const aria = {}; + if (el.hasAttributes && el.hasAttributes()) { + for (const a of el.attributes) { + if (a.name && a.name.startsWith("aria-")) aria[a.name] = a.value; + } + } + + const kids = []; + const children = el.children ? Array.from(el.children) : []; + for (let i = 0; i < children.length && i < maxChildren; i++) { + const child = snap(children[i], depth + 1); + if (child) kids.push(child); + } + + return { + tag, id, classes, role, type, href, text, + aria: Object.keys(aria).length ? aria : undefined, + children: kids.length ? kids : undefined + }; + } + + return snap(root, 0); + })(); + `, + undefined, + { rootSelector, maxDepth, maxChildren, maxTextLen } + ); + + return mcpResult( + { sessionId, rootSelector: rootSelector ?? null, tree }, + tree && !tree.error ? "DOM snapshot captured." : + tree?.error === "root-not-found" ? "DOM root not found." : + "DOM snapshot returned no data." + ); + } +); + +// ── webdriver.findElements (spec-accurate & minimal) ───────────────────────── +server.tool( + "webdriver.findElements", + "Direct WebDriver lookup using the standard locator strategies. " + + "Pass { using, value } exactly per the spec: 'css selector' | 'link text' | 'partial link text' | 'tag name' | 'xpath'. " + + "Returns elementIds for any matches.", + { + sessionId: z + .string() + .min(1) + .describe("Existing WebDriver session id"), + using: z + .enum(["css selector", "link text", "partial link text", "tag name", "xpath"]) + .describe("Locator strategy (WebDriver exact string)"), + value: z + .string() + .min(1) + .describe("Locator value (selector/xpath/text/tag)") + }, + async ({ sessionId, using, value }) => { + const session = sessions.get(sessionId); + if (!session) { + throw new Error(`Session not found: ${sessionId}`); + } + + // Call WebDriver as-is + const found = await session.findElements({ using, value } as any); + + // Return only elementIds (minimal); easy to compose with clickElement later. + const elements = found.map(el => ({ elementId: el.elementId })); + + const payload = { + sessionId, + using, + value, + count: elements.length, + elements + }; + + const summary = + elements.length === 0 + ? "Found 0 elements" + : `Found ${elements.length} element${elements.length === 1 ? "" : "s"}`; + + return mcpResult(payload, summary); + } +); + +// ── webdriver.clickElement ───────────────────────────────── +server.tool( + "webdriver.clickElement", + "Click an element by WebDriver element id within a session. " + + "LLM: Typically obtain elementId from webdriver.selectElements when unique==true.", + { + sessionId: z + .string() + .min(1) + .describe("Existing WebDriver session id"), + elementId: z + .string() + .min(1) + .describe("WebDriver element id (e.g., from webdriver.selectElements)") + }, + async ({ sessionId, elementId }) => { + const session = sessions.get(sessionId); + if (!session) throw new Error(`Session not found: ${sessionId}`); + const el = new Element(session, elementId); + await el.click(); + return mcpResult({ sessionId, elementId, clicked: true }, `Clicked element ${elementId}`); + } +); + +// ── webdriver.closeSession ──────────────────────────────── +server.tool( + "webdriver.closeSession", + "Dispose (delete) a WebDriver session. " + + "LLM: Always close sessions you created when done to keep the environment clean.", + { + sessionId: z + .string() + .min(1) + .describe("Existing WebDriver session id to close") + }, + async ({ sessionId }) => { + const session = sessions.get(sessionId); + if (!session) throw new Error(`Session not found: ${sessionId}`); + await session.dispose(); + sessions.delete(sessionId); + return mcpResult({ sessionId, closed: true }, `Closed session ${sessionId}`); + } +); + +// ── framework.read ──────────────────────────────────────── +server.tool( + "framework.read", + "Read the shipped Roadkill framework source file for reference. " + + "LLM: Use this to understand available WebDriver/ChromeDriver APIs when authoring Jest tests.", + { + file: z + .enum(["webdriver", "chromedriver"]) + .describe("Which framework file to read") + }, + async ({ file }) => { + const { path, content } = await readSiblingModuleBase(file); + return mcpResult( + { file, path, length: content.length, content }, + `Read ${file} framework source from ${path} (${content.length} chars).` + ); + } +); + +// ── framework.exampleTest ───────────────────────────────── +server.tool( + "framework.exampleTest", + "Return a minimal Jest project (package.json, tsconfig.json, example.spec.ts) using Roadkill's ChromeDriver + WebDriverClient. LLM: Use as a template and fill in selectors you discovered via the DOM tools.", + {}, + async () => { + const files = [ + { + filename: "package.json", + content: `{ + "name": "jest-web", + "version": "0.1.4", + "description": "Example using jest and roadkill", + "type": "module", + "private": "true", + "scripts": { + "test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@progress/roadkill": "^0.2.4", + "@types/jest": "^29.5.5", + "@types/node": "^20.8.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + }, + "license": "MIT", + "jest": { + "preset": "ts-jest/presets/default-esm", + "testEnvironment": "@progress/roadkill/jest-environment.ts", + "reporters": [ + "summary" + ] + } +}` + }, + { + filename: "tsconfig.json", + content: `{ + "compilerOptions": { + "module": "Node16", + "target": "ESNext", + "moduleResolution": "Node16", + "esModuleInterop": true + } +}` + }, + { + filename: "example.spec.ts", + content: +`import { ChromeDriver } from "@progress/roadkill/chromedriver.js"; +import { WebDriverClient, Session, by } from "@progress/roadkill/webdriver.js"; +import { describe, test, beforeAll, afterAll, expect } from "@jest/globals"; +import { writeFile, mkdir } from "fs/promises"; +import { join } from "path"; + +describe("example.com smoke", () => { + let chromedriver: ChromeDriver; + let webdriver: WebDriverClient; + let session: Session; + + beforeAll(async () => { + chromedriver = new ChromeDriver({ args: ["--port=9515"] }); + await chromedriver.start(); + webdriver = new WebDriverClient({ address: chromedriver.address }); + session = await webdriver.newSession({ + capabilities: { timeouts: { implicit: 2000 } } + }); + }, 30000); + + afterAll(async () => { try { await session?.dispose(); } finally { await chromedriver?.dispose(); } }, 20000); + + test("navigate and click 'More information...'", async () => { + await session.navigateTo("https://example.com"); + + // Try a robust selector: link text + const link = await session.findElement(by.link("More information...")); + await link.click(); + + const png = await session.takeScreenshot(); + const dir = join("dist","test","example"); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir,"screenshot.png"), png, { encoding: "base64" }); + + const url = await session.getCurrentUrl(); + expect(url).toContain("iana.org"); + }, 20000); +});` + } + ]; + + return mcpResult( + { files, note: "Write these files into a new example folder, run `pnpm i` or `npm i`, then `npm test`." }, + `Returned Jest project with 3 files.` + ); + } +); + +// ─────────────────────────────────────────────────────────── +// Transport + cleanup +// ─────────────────────────────────────────────────────────── +const transport = new StdioServerTransport(); + +transport.onclose = async () => { + try { + for (const [id, s] of sessions) { + try { await s.dispose(); } catch {} + sessions.delete(id); + } + await driver?.dispose(); + } catch {} +}; + +process.on("SIGINT", async () => { + try { + for (const [id, s] of sessions) { try { await s.dispose(); } catch {} sessions.delete(id); } + await driver?.dispose(); + } finally { process.exit(0); } +}); + +process.on("SIGTERM", async () => { + try { + for (const [id, s] of sessions) { try { await s.dispose(); } catch {} sessions.delete(id); } + await driver?.dispose(); + } finally { process.exit(0); } +}); + +await server.connect(transport); diff --git a/packages/@progress/roadkill/semantic-jsx.ts b/packages/@progress/roadkill/semantic-jsx.ts new file mode 100644 index 0000000..7e8fa59 --- /dev/null +++ b/packages/@progress/roadkill/semantic-jsx.ts @@ -0,0 +1,159 @@ +import { SemanticObject } from "./semantic.js"; + +/** + * Classic JSX factory: + * -> SemanticJSX.createElement(Foo, { a: "b" }, ...) + */ +export namespace SemanticJSX { + export type Element = SemanticObject; + + const RESERVED = new Set(["children", "element", "session"]); + const isReserved = (k: string) => RESERVED.has(k); + + function definePropsFromRecord(node: SemanticObject, rec: Record) { + for (const [k, v] of Object.entries(rec)) { + if (isReserved(k)) continue; + if (typeof v === "function") continue; + if (Object.prototype.hasOwnProperty.call(node, k)) continue; + Object.defineProperty(node, k, { + value: v, + writable: false, + enumerable: true, + configurable: true, + }); + } + } + + export function createElement( + type: new () => SemanticObject, + props: Record | null, + ...children: unknown[] + ): SemanticObject { + const node = new type(); + + if (props) definePropsFromRecord(node, props); + + (node as any).children = children + .flat() + .filter((c): c is SemanticObject => c instanceof SemanticObject); + + return node; + } +} + +// Strong JSX prop typing from instance type (with children) + +type Primitiveish = string | number | boolean | null | undefined; +type PrimitiveArray = ReadonlyArray | Primitiveish[]; +type AllowedValue = + T extends Primitiveish ? T : + T extends PrimitiveArray ? T : + never; + +type ExcludedKeys = "element" | "session" | "children"; +type AllowedKeysOf = { + [K in keyof T]-?: + K extends ExcludedKeys ? never : + T[K] extends (...args: any[]) => any ? never : + AllowedValue extends never ? never : K +}[keyof T]; + +type PropsOf = + C extends new (...args: any[]) => any + ? Partial, AllowedKeysOf>>> + : never; + +type WithChildren

= P & { + children?: SemanticObject | ReadonlyArray; +}; + +declare global { + namespace JSX { + type Element = SemanticObject; + interface ElementChildrenAttribute { children: {}; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type LibraryManagedAttributes = + C extends new (...args: any[]) => SemanticObject + ? WithChildren> + : P; + } +} + +export { }; // keep module + +// Assert / Compare helpers + +function isPrimitiveish(v: unknown): v is Primitiveish { + return v == null || ["string", "number", "boolean"].includes(typeof v as string); +} +function isPrimitiveArray(v: unknown): v is PrimitiveArray { + return Array.isArray(v) && v.every(isPrimitiveish); +} + +function pickSerializableProps(node: SemanticObject): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(node as any)) { + if (k === "children" || k === "element" || k === "session") continue; + if (v == null) { out[k] = v; continue; } + const t = typeof v; + if (t === "string" || t === "number" || t === "boolean") { out[k] = v; continue; } + if (Array.isArray(v) && v.every(x => x == null || ["string", "number", "boolean"].includes(typeof x))) { + out[k] = v; continue; + } + } + return out; +} + +function classNameOf(node: SemanticObject): string { + return (node as any)?.constructor?.name || ""; +} + +function diffProps(path: string, a: Record, b: Record): string[] { + const diffs: string[] = []; + const keys = new Set([...Object.keys(a), ...Object.keys(b)]); + for (const k of keys) { + const av = a[k]; + const bv = b[k]; + const same = + (isPrimitiveish(av) && isPrimitiveish(bv) && av === bv) || + (isPrimitiveArray(av) && isPrimitiveArray(bv) && av.length === bv.length && av.every((v, i) => v === bv[i])); + if (!same) { + diffs.push(`${path}: prop "${k}" differs: ${JSON.stringify(av)} !== ${JSON.stringify(bv)}`); + } + } + return diffs; +} + +/** Compare two SemanticObject trees — expected first, actual second. */ +export function semanticEqual(expected: SemanticObject, actual: SemanticObject): { ok: boolean; diff: string[] } { + const diffs: string[] = []; + + function walk(e: SemanticObject, a: SemanticObject, path: string) { + const eName = classNameOf(e); + const aName = classNameOf(a); + if (eName !== aName) { diffs.push(`${path}: type differs: ${eName} !== ${aName}`); return; } + + const eProps = pickSerializableProps(e); + const aProps = pickSerializableProps(a); + diffs.push(...diffProps(`${path}<${eName}>`, eProps, aProps)); + + const eKids = e.children ?? []; + const aKids = a.children ?? []; + if (eKids.length !== aKids.length) { + diffs.push(`${path}<${eName}>: children count differs: ${eKids.length} !== ${aKids.length}`); + } + const count = Math.min(eKids.length, aKids.length); + for (let i = 0; i < count; i++) { + walk(eKids[i], aKids[i], `${path}/${eName}[${i}]`); + } + } + + walk(expected, actual, ""); + return { ok: diffs.length === 0, diff: diffs }; +} + +/** Throw if trees don’t match. Order: expected, actual. */ +export function expectSemanticMatch(actual: SemanticObject, expected: SemanticObject): void { + const { ok, diff } = semanticEqual(expected, actual); + if (!ok) throw new Error(["Semantic trees differ:", ...diff.map(d => ` - ${d}`)].join("\n")); +} diff --git a/packages/@progress/roadkill/semantic.ts b/packages/@progress/roadkill/semantic.ts new file mode 100644 index 0000000..595d7a8 --- /dev/null +++ b/packages/@progress/roadkill/semantic.ts @@ -0,0 +1,331 @@ +import { Session, type Element as WebDriverElement } from "@progress/roadkill/webdriver.js"; + +type FinderSrc = { name: string; src: string }; + +class Registry { + private readonly list: FinderSrc[] = []; + private readonly ctors = new Map>(); + register(ctor: SemanticCtor) { + const name = ctor?.name; + const fn = (ctor as any)?.find; + if (!name || typeof fn !== "function") { + throw new Error(`@semantic: ${name || ""} must declare a static find() function`); + } + (ctor as any).semanticClass = name; + this.list.push({ name, src: fn.toString() }); + this.ctors.set(name, ctor); + } + payload(): FinderSrc[] { return this.list.slice(); } + ctorOf(name: string) { return this.ctors.get(name); } +} +const registry = new Registry(); + +export function semantic() { + return function any>(ctor: T) { + registry.register(ctor as unknown as SemanticCtor); + return ctor; + }; +} +export function getFinders(): FinderSrc[] { return registry.payload(); } + +/** Base class: exposes hydrated WebDriver element and children. */ +export class SemanticObject { + public readonly children: SemanticObject[] = []; + public element?: WebDriverElement | null; + constructor(public readonly session?: Session) { } + + childrenOfType(ctor: new (...args: any[]) => T): T[] { + return this.children.filter(c => c instanceof ctor) as T[]; + } + + // serialization reads from instance props + + private static pickSerializableProps(node: SemanticObject): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(node as any)) { + if (k === "children" || k === "element" || k === "session") continue; + if (v == null) { out[k] = v; continue; } + const t = typeof v; + if (t === "string" || t === "number" || t === "boolean") { out[k] = v; continue; } + if (Array.isArray(v) && v.every(x => x == null || ["string", "number", "boolean"].includes(typeof x))) { + out[k] = v; continue; + } + } + return out; + } + + toJSON(): any { + const cls = (this as any).constructor.name; + const clean = SemanticObject.pickSerializableProps(this); + return [cls, clean, ...this.children.map(c => c.toJSON())]; + } + + toXML(indent: undefined | string = undefined): string { + const pretty = indent != undefined; + const esc = (s: string) => + s.replace(/&/g, "&").replace(//g, ">") + .replace(/"/g, """).replace(/'/g, "'"); + + const attrs = (props: Record): string => + Object.entries(props || {}) + .filter(([k, v]) => + k !== "element" && k !== "children" && + v !== null && v !== undefined && (typeof v !== "object")) + .map(([k, v]) => ` ${k}="${esc(String(v))}"`) + .join(""); + + const xmlOf = (node: SemanticObject, level: number): string => { + const cls = (node as any).constructor.name; + const rest = SemanticObject.pickSerializableProps(node); + const a = attrs(rest); + const kids = node.children ?? []; + + if (!pretty) { + if (kids.length === 0) return `<${cls}${a}/>`; + return `<${cls}${a}>${kids.map(c => (c as any).toXML(undefined)).join("")}`; + } + + const pad = (indent as string).repeat(level); + if (kids.length === 0) return `${pad}<${cls}${a}/>`; + const open = `${pad}<${cls}${a}>`; + const inner = kids.map(c => xmlOf(c, level + 1)).join("\n"); + const close = `${pad}`; + return `${open}\n${inner}\n${close}`; + }; + + return xmlOf(this, 0); + } +} + +export class Root extends SemanticObject { } + +export interface SemanticCtor { + new(session: Session): T; + semanticClass: string; + find(): Array<{ element: globalThis.Element;[k: string]: any }>; +} + +type ElementOf = T extends (infer U)[] ? U : never; + +type HydrateElements = + T extends globalThis.Element ? WebDriverElement : + T extends (infer U)[] ? HydrateElements[] : + T extends object ? { [K in keyof T]: HydrateElements } : + T; + +export type DTO = { element: globalThis.Element } & T; + +export type AnnotatedDTO = HydrateElements & { + ["$semantic-class"]: string; + children?: Array>; +}; + +export type DTOOf any }> = + AnnotatedDTO>>; + +/** ---------- Browser runner (stringified) ---------- */ +function browserRunner(list: Array<{ name: string; src: string }>) { + function findElementsByCss(selector: string, mapFn: (el: Element) => any) { + const out: any[] = []; + for (const el of document.querySelectorAll(selector)) { + const r = mapFn(el); + if (!r) continue; + if (typeof r !== "object") continue; + (r as any).element = el; + out.push(r); + } + return out; + } + + function compileFinder(src: string): Function { + let e1: any, e2: any, e3: any; + try { return (0, eval)(`(${src})`); } catch (err) { e1 = err; } + try { return (0, eval)(`(function ${src})`); } catch (err) { e2 = err; } + try { return (0, eval)(`({ ${src} }).find`); } catch (err) { + e3 = err; + throw new Error( + `Failed to compile static find():\n- as expression: ${e1?.message || e1}\n- with 'function' prefix: ${e2?.message || e2}\n- as object method: ${e3?.message || e3}\nSource:\n${src}` + ); + } + } + + const flat: any[] = []; + for (const { name, src } of list) { + const f = compileFinder(src); + let arr: any[] = []; + try { arr = f() || []; } catch (err: any) { + throw new Error(`Finder "${name}" threw: ${err?.message || err}\nSource:\n${src}`); + } + for (const dto of arr) flat.push({ ["$semantic-class"]: name, ...dto, children: [] }); + } + + const contains = (a: Element | null | undefined, b: Element | null | undefined) => + !!(a && b && a !== b && a.contains(b)); + + for (let i = 0; i < flat.length; i++) { + let bestParent: any | null = null; + const child = flat[i]; + for (let j = 0; j < flat.length; j++) { + if (i === j) continue; + const maybe = flat[j]; + if (!contains(maybe.element, child.element)) continue; + if (!bestParent) bestParent = maybe; + else if (contains(bestParent.element, maybe.element)) bestParent = maybe; + } + if (bestParent) bestParent.children.push(child); + } + + const childSet = new Set(); + for (const n of flat) for (const c of n.children) childSet.add(c); + return flat.filter(n => !childSet.has(n)); +} + +export function buildDiscoverScript(): string { + let script = `// utility functions\n`; + script += findElementsByCss.toString() + `\n`; + + const finders: { name: string, find: Function }[] = undefined; + script += `\n// finder functions array\nconst finders = [\n`; + for (let finder of getFinders()) { + script += `{\n name: ${JSON.stringify(finder.name)},\n ${finder.src}\n},`; + } + script += `];\n`; + + script += `\n// algorithm to collect DTO objects\n` + function findElements() { + let elements = []; + for (let { name, find } of finders) { + for (let item of find()) { + item["$semantic-class"] = name; + elements.push(item); + } + } + return elements; + }.toString() + `\n`; + + script += `\n` + function buildTree(elements) { + const flat = elements.map(n => ({ ...n, children: [] })); + for (let i = 0; i < flat.length; i++) { + const child = flat[i]; + const childEl = child.element; + let bestParent = null; + for (let j = 0; j < flat.length; j++) { + if (i === j) continue; + const maybe = flat[j]; + const parentEl = maybe.element; + if (!parentEl || !childEl || parentEl === childEl) continue; + if (!parentEl.contains(childEl)) continue; + if (!bestParent) bestParent = maybe; + else if (bestParent.element && bestParent.element.contains(parentEl)) bestParent = maybe; + } + child.__parent = bestParent || null; + if (bestParent) bestParent.children.push(child); + } + const roots = flat.filter(n => !n.__parent); + for (const n of flat) delete n.__parent; + return roots; + }.toString() + `\n`; + + script += `\nreturn buildTree(findElements());`; + return script; +} + +/** Hydrate a DTO forest (from browser) into a Root tree. */ +export function hydrate(session: Session, dtoRoots: Array>): Root { + const root = new Root(session); + + const RESERVED = new Set(["children", "element", "$semantic-class"]); + function definePropsFromDto(inst: SemanticObject, dto: Record) { + for (const [k, v] of Object.entries(dto)) { + if (RESERVED.has(k)) continue; + if (typeof v === "function") continue; + if (Object.prototype.hasOwnProperty.call(inst, k)) continue; + Object.defineProperty(inst, k, { + value: v, + writable: false, + enumerable: true, + configurable: true, + }); + } + } + + function makeNode(dto: AnnotatedDTO): SemanticObject { + const ctor = registry.ctorOf(dto["$semantic-class"]); + const inst = ctor ? new ctor(session) : new SemanticObject(session); + + (inst as any).element = (dto as any).element ?? null; // attach WD element + definePropsFromDto(inst, dto as any); // copy DTO props + + const kids = dto.children || []; + for (const child of kids) inst.children.push(makeNode(child)); + return inst; + } + + for (const dto of dtoRoots || []) root.children.push(makeNode(dto)); + return root; +} + +/** Discover: build → execute → hydrate (no debug logging). */ +export async function discover(session: Session): Promise { + return hydrate(session, await session.executeScript(buildDiscoverScript())); +} + +/** Browser-side helper, re-exported for user finders. */ +export function findElementsByCss( + selector: string, + mapFn: (el: globalThis.Element) => T | undefined +): Array<{ element: globalThis.Element } & Omit> { + const out: any[] = []; + for (const el of document.querySelectorAll(selector)) { + const r = mapFn(el); + if (!r) continue; + if (typeof r !== "object") continue; + (r as any).element = el; + out.push(r); + } + return out; +} + +// what to ignore when mapping instance → DTO +type RuntimeKeys = "children" | "element" | "session"; + +/* primitive-ish */ +type Primitiveish = string | number | boolean | null | undefined; + +/* map WebDriverElement → Element (and recurse through arrays/objects) */ +type ToFinderValue = + // direct WD element (nullable/optional also supported) + T extends WebDriverElement | null | undefined ? (Element | null) : + // arrays (readonly or mutable) + T extends readonly (infer U)[] ? ReadonlyArray> : + T extends (infer U)[] ? ToFinderValue[] : + // objects: map each field + T extends object ? { [K in keyof T]: ToFinderValue } : + // primitives pass through + T extends Primitiveish ? T : + // everything else is not representable in a finder DTO + never; + +/* pick only DTO-like keys from an instance type */ +type AllowedDTOKeysOf = { + [K in keyof T]-?: + // strip runtime-only + K extends RuntimeKeys ? never : + // drop methods + T[K] extends (...args: any[]) => any ? never : + // keep only things that can become ToFinderValue (not never) + [ToFinderValue] extends [never] ? never : K +}[keyof T]; + +/** + * The DTO shape produced by `find()` for a given class constructor. + * - Always includes `element: Element` + * - Includes only allowed instance props + * - Converts WebDriverElement → Element (recursively) + */ +export type Find SemanticObject> = + { element: Element } & + { [K in AllowedDTOKeysOf>]: ToFinderValue[K]> }; + +/** Convenience: fields-only part (without the required `element`), suitable for use in findElementsByCss */ +export type FindByCSSFields SemanticObject> = + Omit, "element">; diff --git a/packages/@progress/roadkill/server.ts b/packages/@progress/roadkill/server.ts index 280a649..c401be4 100644 --- a/packages/@progress/roadkill/server.ts +++ b/packages/@progress/roadkill/server.ts @@ -16,6 +16,11 @@ export interface ServerOptions { * A console.log prefix. */ logPrefix?: string; + + /** + * A console.log implementation. + */ + log?: (line: string) => void; } /** @@ -128,7 +133,7 @@ export abstract class Server extends EventEmitter } protected log(line: string) { - if (this.options.enableLogging) console.log(`${this.prefix ? "[" + this.prefix + "] " : ""}${line}`); + if (this.options.enableLogging) (this.options.log ?? console.log)(`${this.prefix ? "[" + this.prefix + "] " : ""}${line}`); } private onStdOut(line: string) { diff --git a/packages/@progress/roadkill/tsconfig.json b/packages/@progress/roadkill/tsconfig.json index 4cb6283..2854e73 100644 --- a/packages/@progress/roadkill/tsconfig.json +++ b/packages/@progress/roadkill/tsconfig.json @@ -3,6 +3,7 @@ "target": "ESNext", "module": "Node16", "moduleResolution": "Node16", - "sourceMap": true + "sourceMap": true, + "skipLibCheck": true } } \ No newline at end of file diff --git a/packages/@progress/roadkill/webdriver.ts b/packages/@progress/roadkill/webdriver.ts index bb3d5f4..966183b 100644 --- a/packages/@progress/roadkill/webdriver.ts +++ b/packages/@progress/roadkill/webdriver.ts @@ -16,7 +16,7 @@ export class WebDriverMethodError extends Error { constructor(error?: string, options?: ErrorOptions, args?: {}) { super(error, options); if (args) - for(const key in args) + for (const key in args) this[key] = args[key]; } } @@ -29,7 +29,7 @@ export class WebDriverRequestError extends Error { if (error.value.stacktrace) Object.defineProperty(this, "stacktrace", { value: error.value.stacktrace, enumerable: false }); if (error.value.data) - this["data"] = error.value.data; + this["data"] = error.value.data; } } } @@ -467,6 +467,7 @@ export interface WebDriverClientOptions { address: string; enableLogging?: boolean; logPrefix?: string; + log?: (line: string) => void; } /** @@ -492,10 +493,10 @@ export class WebDriverClient { public constructor(public readonly options: WebDriverClientOptions, public readonly fetchImplementation: typeof fetch = fetch) { } - get prefix() { return this.options.logPrefix ?? "[WebDriverClient]" } + get prefix() { return this.options.logPrefix ?? "[WebDriverClient]"; } protected log(line: string) { - if (this.options?.enableLogging) console.log(`${this.prefix ? this.prefix + " " : ""}${line}`); + if (this.options?.enableLogging) (this.options.log ?? console.log)(`${this.prefix ? this.prefix + " " : ""}${line}`); } /** @@ -505,11 +506,11 @@ export class WebDriverClient { public async newSession(options: ProcessCapabilities | { capabilities: ValidateCapabilities }, signal?: AbortSignal): Promise { try { const result = await this.request< - ProcessCapabilities | { capabilities: ValidateCapabilities }, - { capabilities: MatchingCapabilities, sessionId: string } - >("POST", "/session", options, signal); + ProcessCapabilities | { capabilities: ValidateCapabilities }, + { capabilities: MatchingCapabilities, sessionId: string } + >("POST", "/session", options, signal); return new Session(this, result.sessionId, result.capabilities); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to create a new session.`, { cause }, { options }); } } @@ -521,7 +522,7 @@ export class WebDriverClient { public async status(signal?: AbortSignal): Promise<{ ready: boolean, message: string, [other: string]: any }> { try { return await this.request<{}, { ready: boolean, message: string }>("GET", "/status", undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError("Failed to retrieve status.", { cause }); } } @@ -536,14 +537,14 @@ export class WebDriverClient { headers["Content-Type"] = "application/json; charset=utf-8"; requestInit.body = JSON.stringify(args, withReplacer(serializer)); } - + const bodyStr = typeof requestInit.body === "string" ? requestInit.body.slice(0, 40) : ""; this.log(`fetch: ${method} ${uri}${bodyStr ? " " + bodyStr : ""}`); const response = await this.fetchImplementation(`${this.options.address}${uri}`, requestInit); this.log(` response: ${method} ${response.status} ${response.statusText} ${uri}${bodyStr ? " " + bodyStr : ""}`); let text = ""; - try { text = await response.text(); } catch {} + try { text = await response.text(); } catch { } if (!response.ok) { if (text) { @@ -568,7 +569,7 @@ export class WebDriverClient { const result = (json as { value: Result }).value; return result; - } catch(cause) { + } catch (cause) { const error = cause instanceof WebDriverRequestError ? cause : new WebDriverRequestError("WebDriver API call failed.", { cause }); error["address"] = this.options.address; error["command"] = `${method} ${uri}`; @@ -636,7 +637,7 @@ export class Session implements Disposable, Serializer { public async getTimeouts(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/timeouts`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get timeouts."`, { cause }); } } @@ -647,21 +648,18 @@ export class Session implements Disposable, Serializer { public async setTimeouts(timeouts: TimeoutsConfiguration, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/timeouts`, timeouts, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to set timeouts.`, { cause }, { timeouts }); } } /** * [10.1 Navigate To](https://www.w3.org/TR/webdriver2/#navigate-to) - * - * The command causes the user agent to navigate the current top-level browsing context to a new location. - * @param url */ public async navigateTo(url: string, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/url`, { url }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to navigate to ${url}.`, { cause }, { url }); } } @@ -672,86 +670,74 @@ export class Session implements Disposable, Serializer { public async getCurrentUrl(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/url`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get current url.`, { cause }); } } /** * [10.3 Back](https://www.w3.org/TR/webdriver2/#back) - * - * This command causes the browser to traverse one step backward in the joint session history of the current top-level browsing context. This is equivalent to pressing the back button in the browser chrome or invoking window.history.back. */ public async back(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/back`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/back`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to go back.`, { cause }); } } /** * [10.4 Forward](https://www.w3.org/TR/webdriver2/#forward) - * - * This command causes the browser to traverse one step forwards in the joint session history of the current top-level browsing context. This is equivalent to pressing the forward button in the browser chrome or invoking window.history.forward. */ public async forward(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/forward`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/forward`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to go forward.`, { cause }); } } /** * [10.5 Refresh](https://www.w3.org/TR/webdriver2/#refresh) - * - * This command causes the browser to reload the page in the current top-level browsing context. */ public async refresh(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/refresh`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/refresh`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to refresh.`, { cause }); } } /** * [10.6 Get Title](https://www.w3.org/TR/webdriver2/#get-title) - * - * This command returns the document title of the current top-level browsing context, equivalent to calling document.title. */ public async getTitle(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/title`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get title.`, { cause }); } } /** * [11.1 Get Window Handle](https://www.w3.org/TR/webdriver2/#get-window-handle) - * - * Return the window associated with the current top-level browsing context. */ public async getWindow(signal?: AbortSignal): Promise { try { const handle = await this.request<{}, WindowHandle>("GET", `/session/${this.sessionId}/window`, undefined, signal); return new Window(this, handle); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get window.`, { cause }); } } /** * [11.2 Close Window](https://www.w3.org/TR/webdriver2/#close-window) - * - * Close the current top-level browsing context. */ public async closeWindow(signal?: AbortSignal): Promise { try { return await this.request("DELETE", `/session/${this.sessionId}/window`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to close window.`, { cause }); } } @@ -763,7 +749,7 @@ export class Session implements Disposable, Serializer { try { const handles = await this.request<{}, WindowHandle[]>("GET", `/session/${this.sessionId}/window/handles`, undefined, signal); return handles.map(handle => new Window(this, handle)); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get windows.`, { cause }); } } @@ -773,100 +759,96 @@ export class Session implements Disposable, Serializer { */ public async newWindow(type: "tab" | "window" = "tab", signal?: AbortSignal): Promise { try { - const res = await this.request<{ type: "tab" | "window" }, { handle: WindowHandle, type: "tab" | "window"}>("POST", `/session/${this.sessionId}/window/new`, { type }, signal); + const res = await this.request<{ type: "tab" | "window" }, { handle: WindowHandle, type: "tab" | "window" }>("POST", `/session/${this.sessionId}/window/new`, { type }, signal); return new Window(this, res.handle, res.type); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to open a new window.`, { cause }, { type }); } } /** - * [11.6 Switch To Frame](11.6 Switch To Frame) - * - * The Switch To Frame command is used to select the current top-level browsing context or a child browsing context of the current browsing context to use as the current browsing context for subsequent commands. - * - * WebDriver is not bound by the same origin policy, so it is always possible to switch into child browsing contexts, even if they are different origin to the current browsing context. + * [11.6 Switch To Frame](https://www.w3.org/TR/webdriver2/#switch-to-frame) */ - public async switchToFrame(frameId: null | number | ElementId = null, signal?: AbortSignal): Promise { + public async switchToFrame(frame: null | number | Element | WebElementReference, signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/frame`, { id: frameId }, signal); - } catch(cause) { - throw new WebDriverMethodError(`Failed to switch to frame ${frameId}.`, { cause }, { frameId }); + let id: null | number | WebElementReference; + if (frame === null || typeof frame === "number") { + id = frame; + } else if (frame instanceof Element) { + id = { [webElementIdentifier]: frame.elementId }; + } else if (typeof frame === "object" && webElementIdentifier in frame) { + id = frame; + } else { + throw new Error("Invalid frame reference"); + } + return await this.request("POST", `/session/${this.sessionId}/frame`, { id }, signal); + } catch (cause) { + throw new WebDriverMethodError(`Failed to switch to frame.`, { cause }, { frame }); } } /** * [11.7 Switch To Parent Frame](https://www.w3.org/TR/webdriver2/#switch-to-parent-frame) - * - * The Switch to Parent Frame command sets the current browsing context for future commands to the parent of the current browsing context. */ public async switchToParentFrame(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/frame/parent`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/frame/parent`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to switch to parent frame.`, { cause }); } } /** * [11.8.1 Get Window Rect](https://www.w3.org/TR/webdriver2/#get-window-rect) - * - * The Get Window Rect command returns the size and position on the screen of the operating system window corresponding to the current top-level browsing context. */ public async getWindowRect(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/window/rect`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get window rect.`, { cause }); } } /** * [11.8.2 Set Window Rect](https://www.w3.org/TR/webdriver2/#set-window-rect) - * - * The Set Window Rect command alters the size and the position of the operating system window corresponding to the current top-level browsing context. */ public async setWindowRect(windowRect: { x: null | number, y: null | number, width: null | number, height: null | number }, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/window/rect`, windowRect, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to set window rect.`, { cause }, { windowRect }); } } /** * [11.8.3 Maximize Window](https://www.w3.org/TR/webdriver2/#maximize-window) - * - * The Maximize Window command invokes the window manager-specific “maximize” operation, if any, on the window containing the current top-level browsing context. This typically increases the window to the maximum available size without going full-screen. */ public async maximize(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/window/maximize`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/window/maximize`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to maximize.`, { cause }); } } /** * [11.8.4 Minimize Window](https://www.w3.org/TR/webdriver2/#minimize-window) - * - * The Minimize Window command invokes the window manager-specific “minimize” operation, if any, on the window containing the current top-level browsing context. This typically hides the window in the system tray. */ public async minimize(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/window/minimize`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/window/minimize`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to minimize.`, { cause }); } } - + /** * [11.8.5 Fullscreen Window](https://www.w3.org/TR/webdriver2/#fullscreen-window) */ public async fullscreen(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/window/fullscreen`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/window/fullscreen`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to go fullscreen.`, { cause }); } } @@ -877,7 +859,7 @@ export class Session implements Disposable, Serializer { public async findElement(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/element`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find element by ${lookup.using} "${lookup.value}".`, { cause }, { lookup }); } } @@ -888,7 +870,7 @@ export class Session implements Disposable, Serializer { public async findElements(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/elements`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find elements by ${lookup.using} "${lookup.value}".`, { cause }, { lookup }); } } @@ -899,20 +881,18 @@ export class Session implements Disposable, Serializer { public async getActiveElement(signal?: AbortSignal): Promise { try { return await this.element(await this.request<{}, WebElementReference>("GET", `/session/${this.sessionId}/element/active`, undefined, signal)); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get active element.`, { cause }); } } /** * [13.1 Get Page Source](https://www.w3.org/TR/webdriver2/#get-page-source) - * - * The ***Get Page Source*** command returns a string serialization of the DOM of the current browsing context active document. */ public async getPageSource(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/source`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get page source.`, { cause }); } } @@ -923,7 +903,7 @@ export class Session implements Disposable, Serializer { public async executeScript(script: string, signal?: AbortSignal, ...args: any[]): Promise { try { return await this.request("POST", `/session/${this.sessionId}/execute/sync`, { script, args }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to execute script.`, { cause }); } } @@ -934,7 +914,7 @@ export class Session implements Disposable, Serializer { public async executeScriptAsync(script: string, signal?: AbortSignal, ...args: any[]): Promise { try { return await this.request("POST", `/session/${this.sessionId}/execute/async`, { script, args }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to execute script async.`, { cause }); } } @@ -945,9 +925,9 @@ export class Session implements Disposable, Serializer { public async getCookies(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/cookie`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get cookies.`, { cause }); - } + } } /** @@ -956,7 +936,7 @@ export class Session implements Disposable, Serializer { public async getNamedCookie(name: string, signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/cookie/${name}`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get named cookie '${name}'.`, { cause }, { name }); } } @@ -967,7 +947,7 @@ export class Session implements Disposable, Serializer { public async addCookie(cookie: Cookie, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/cookie`, { cookie }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to add cookie '${cookie.name}'.`, { cause }, { cookie }); } } @@ -978,7 +958,7 @@ export class Session implements Disposable, Serializer { public async deleteCookie(name: string, signal?: AbortSignal): Promise { try { return await this.request("DELETE", `/session/${this.sessionId}/cookie/${name}`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to delete cookie '${name}'.`, { cause }, { name }); } } @@ -989,7 +969,7 @@ export class Session implements Disposable, Serializer { public async deleteAllCookies(signal?: AbortSignal): Promise { try { return await this.request("DELETE", `/session/${this.sessionId}/cookie`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to delete all cookies.`, { cause }); } } @@ -1000,20 +980,18 @@ export class Session implements Disposable, Serializer { public async performActions(actions: ActionSequence[], signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/actions`, { actions }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to perform actions.`, { cause }); } } /** * [15.8 Release Actions](https://www.w3.org/TR/webdriver2/#release-actions) - * - * The Release Actions command is used to release all the keys and pointer buttons that are currently depressed. This causes events to be fired as if the state was released by an explicit series of actions. It also clears all the internal state of the virtual devices. */ public async releaseActions(signal?: AbortSignal): Promise { try { return await this.request("DELETE", `/session/${this.sessionId}/actions`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to release actions.`, { cause }); } } @@ -1023,8 +1001,8 @@ export class Session implements Disposable, Serializer { */ public async dismissAlert(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/alert/dismiss`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/alert/dismiss`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to dismiss alert.`, { cause }); } } @@ -1034,8 +1012,8 @@ export class Session implements Disposable, Serializer { */ public async acceptAlert(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/alert/accept`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/alert/accept`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to accept alert.`, { cause }); } } @@ -1046,20 +1024,18 @@ export class Session implements Disposable, Serializer { public async getAlertText(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/alert/text`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get alert.`, { cause }); } } /** * [16.4 Send Alert Text](https://www.w3.org/TR/webdriver2/#send-alert-text) - * - * The Send Alert Text command sets the text field of a window.prompt user prompt to the given value. */ public async sendAlertText(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/alert/text`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/alert/text`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to send alert text.`, { cause }); } } @@ -1070,7 +1046,7 @@ export class Session implements Disposable, Serializer { public async takeScreenshot(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/screenshot`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to take screenshot.`, { cause }); } } @@ -1081,7 +1057,7 @@ export class Session implements Disposable, Serializer { public async printPage(printOptions?: PrintOptions, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/print`, printOptions ?? {}, signal) - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError("Failed to print page.", { cause }, printOptions && { printOptions }); } } @@ -1089,14 +1065,14 @@ export class Session implements Disposable, Serializer { /** * Deserialize an {@link Element} by {@link WebElementReference} within this session. */ - public element(elementRef: WebElementReference, signal?: AbortSignal): Element { + public element(elementRef: WebElementReference, _signal?: AbortSignal): Element { return new Element(this, elementRef[webElementIdentifier]); } /** * Deserialize a {@link ShadowRoot} by {@link ShadowRootReference} within this session. */ - public shadowRoot(shadowRootRef: ShadowRootReference, signal?: AbortSignal): ShadowRoot { + public shadowRoot(shadowRootRef: ShadowRootReference, _signal?: AbortSignal): ShadowRoot { return new ShadowRoot(this, shadowRootRef[shadowRootIdentifier]); } } @@ -1116,16 +1092,14 @@ export class Window { public get sessionId() { return this.session.sessionId; } - + /** * [11.3 Switch To Window](https://www.w3.org/TR/webdriver2/#switch-to-window) - * - * Switching window will select the current top-level browsing context used as the target for all subsequent commands. In a tabbed browser, this will typically make the tab containing the browsing context the selected tab. */ public async switchToWindow(signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/window`, { handle: this.handle }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to switch to window ${this.handle}.`, { cause }); } } @@ -1163,12 +1137,13 @@ export class Element implements WebElementReference { } /** - * [11.6 Switch To Frame](https://www.w3.org/TR/webdriver2/#switch-to-frame) + * Switch to this element’s frame */ public async switchToFrame(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/frame`, { id: this.elementId }, signal); - } catch(cause) { + const id: WebElementReference = { [webElementIdentifier]: this.elementId }; + return await this.request("POST", `/session/${this.sessionId}/frame`, { id }, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to switch to frame from element.`, { cause }); } } @@ -1179,7 +1154,7 @@ export class Element implements WebElementReference { public async findElement(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/element`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find element by ${lookup.using} "${lookup.value}" from element.`, { cause }, { lookup }); } } @@ -1190,7 +1165,7 @@ export class Element implements WebElementReference { public async findElements(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/elements`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find elements by ${lookup.using} "${lookup.value}" from element.`, { cause }, { lookup }); } } @@ -1201,7 +1176,7 @@ export class Element implements WebElementReference { public async shadowRoot(signal?: AbortSignal): Promise { try { return await this.request<{}, ShadowRoot>("GET", `/session/${this.sessionId}/element/${this.elementId}/shadow`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get shadowRoot from element.`, { cause }); } } @@ -1212,45 +1187,40 @@ export class Element implements WebElementReference { public async isSelected(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/element/${this.elementId}/selected`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get isSelected from element.`, { cause }); } } /** * [12.4.2 Get Element Attribute](https://www.w3.org/TR/webdriver2/#get-element-attribute) - * - * Please note that the behavior of this command deviates from the behavior of getAttribute() in [DOM], which in the case of a set boolean attribute would return an empty string. The reason this command returns true as a string is because this evaluates to true in most dynamically typed programming languages, but still preserves the expected type information. */ public async getAttribute(name: string, signal?: AbortSignal): Promise { try { return await this.request<{}, null | string>("GET", `/session/${this.sessionId}/element/${this.elementId}/attribute/${name}`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get attribute ${name} from element.`, { cause }, { name }); } } - /** * [12.4.3 Get Element Property](https://www.w3.org/TR/webdriver2/#get-element-property) */ public async getProperty(name: string, signal?: AbortSignal): Promise { try { return await this.request<{}, any>("GET", `/session/${this.sessionId}/element/${this.elementId}/property/${name}`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get property ${name} from element.`, { cause }, { name }); - } + } } /** * [12.4.4 Get Element CSS Value](https://www.w3.org/TR/webdriver2/#get-element-css-value) - * - * The Get Element Text command intends to return an element’s text “as rendered”. An element’s rendered text is also used for locating a elements by their link text and partial link text. */ public async getCSSValue(name: string, signal?: AbortSignal): Promise { try { return await this.request<{}, string>("GET", `/session/${this.sessionId}/element/${this.elementId}/css/${name}`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get css value ${name} from element.`, { cause }, { name }); } } @@ -1261,7 +1231,7 @@ export class Element implements WebElementReference { public async getText(signal?: AbortSignal): Promise { try { return await this.request<{}, string>("GET", `/session/${this.sessionId}/element/${this.elementId}/text`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get text from element.`, { cause }); } } @@ -1272,26 +1242,18 @@ export class Element implements WebElementReference { public async getTagName(signal?: AbortSignal): Promise { try { return await this.request<{}, string>("GET", `/session/${this.sessionId}/element/${this.elementId}/name`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get tag name from element.`, { cause }); } } /** - * The Get Element Rect command returns the dimensions and coordinates of the given web element. The returned value is a dictionary with the following members: - * - {@link ElementRect.x} - * X axis position of the top-left corner of the web element relative to the current browsing context’s document element in CSS pixels. - * - {@link ElementRect.y} - * Y axis position of the top-left corner of the web element relative to the current browsing context’s document element in CSS pixels. - * - {@link ElementRect.height} - * Height of the web element’s bounding rectangle in CSS pixels. - * - {@link ElementRect.width} - * Width of the web element’s bounding rectangle in CSS pixels. + * The Get Element Rect command returns the dimensions and coordinates of the given web element. */ public async getRect(signal?: AbortSignal): Promise { try { return await this.request<{}, ElementRect>("GET", `/session/${this.sessionId}/element/${this.elementId}/rect`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get rect from element.`, { cause }); } } @@ -1302,7 +1264,7 @@ export class Element implements WebElementReference { public async isEnabled(signal?: AbortSignal): Promise { try { return await this.request<{}, boolean>("GET", `/session/${this.sessionId}/element/${this.elementId}/enabled`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get isEnabled from element.`, { cause }); } } @@ -1313,7 +1275,7 @@ export class Element implements WebElementReference { public async getComputedRole(signal?: AbortSignal): Promise { try { return await this.request<{}, string>("GET", `/session/${this.sessionId}/element/${this.elementId}/computedrole`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get computed role from element.`, { cause }); } } @@ -1324,22 +1286,18 @@ export class Element implements WebElementReference { public async getComputedLabel(signal?: AbortSignal): Promise { try { return await this.request<{}, string>("GET", `/session/${this.sessionId}/element/${this.elementId}/computedlabel`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to get computed label from element.`, { cause }); } } /** * [12.5.1 Element Click](https://www.w3.org/TR/webdriver2/#element-click) - * - * The Element Click command scrolls into view the element if it is not already pointer-interactable, and clicks its in-view center point. - * - * If the element’s center point is obscured by another element, an element click intercepted error is returned. If the element is outside the viewport, an element not interactable error is returned. */ public async click(signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/click`, {}, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to click element.`, { cause }); } } @@ -1349,8 +1307,8 @@ export class Element implements WebElementReference { */ public async clear(signal?: AbortSignal): Promise { try { - return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/clear`, undefined, signal); - } catch(cause) { + return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/clear`, {}, signal); + } catch (cause) { throw new WebDriverMethodError(`Failed to clear element.`, { cause }); } } @@ -1361,7 +1319,7 @@ export class Element implements WebElementReference { public async sendKeys(text: string, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/element/${this.elementId}/value`, { text }, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to send text to element.`, { cause }, { text: text?.length > 50 ? text?.substring(0, 50) + "..." : text }); @@ -1374,7 +1332,7 @@ export class Element implements WebElementReference { public async takeScreenshot(signal?: AbortSignal): Promise { try { return await this.request("GET", `/session/${this.sessionId}/element/${this.elementId}/screenshot`, undefined, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to take a screenshot of element.`, { cause }); } } @@ -1407,7 +1365,7 @@ export class ShadowRoot implements ShadowRootReference { public async findElement(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/shadow/${this.shadowId}/element`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find element by ${lookup.using} "${lookup.value}" from shadow root.`, { cause }, { lookup }); } } @@ -1418,7 +1376,7 @@ export class ShadowRoot implements ShadowRootReference { public async findElements(lookup: ElementLookup, signal?: AbortSignal): Promise { try { return await this.request("POST", `/session/${this.sessionId}/shadow/${this.shadowId}/elements`, lookup, signal); - } catch(cause) { + } catch (cause) { throw new WebDriverMethodError(`Failed to find elements by ${lookup.using} "${lookup.value}" from shadow root.`, { cause }, { lookup }); } }