From 1a1430d13554939231c5807195d29d0d632ffbbe Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 8 Oct 2026 14:31:07 -0700 Subject: [PATCH 1/2] chore: use TypeScript parser instead of line matching in update_pw.mjs Replace the line-based stripFileHeader() and the line-based expect-types path rewrite with edits driven by the TypeScript 7 (experimental) API AST. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- CONTRIBUTING.md | 4 +- package-lock.json | 388 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 11 ++ update_pw.mjs | 268 ++++++++++++++++++++++---------- 5 files changed, 590 insertions(+), 83 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/AGENTS.md b/AGENTS.md index 148b9a3..4d30a9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ This is the Try Playwright monorepo. It contains: 2. Reuse existing patterns in the touched service/package. 3. Avoid committing secrets, certificates, or local environment files. 4. Prefer `internal/` for shared Go logic used by multiple services/workers. -5. For Playwright version/autocomplete updates, use `node update_pw.mjs` instead of ad-hoc manual edits. +5. For Playwright version/autocomplete updates, use `npm ci && node update_pw.mjs` (repo root) instead of ad-hoc manual edits. ## Validation Commands diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13a9293..768b00e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Kubernetes manifests to run only a subset, for example `javascript,python`. ## Generate / Update autocompletion -- Execute the `update_pw.mjs` script. +- Execute `npm ci && node update_pw.mjs` in the repository root. ## Infrastructure evolution @@ -73,7 +73,7 @@ The worker infra got rewritten, since the workers would share a lot of code each ## Updating Playwright -1. Execute `node update_pw.mjs` +1. Execute `npm ci && node update_pw.mjs` in the repository root 1. Create and merge the PR 1. Wait until PR is built on the `main` branch 1. Exeute the following on the host: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..955c125 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,388 @@ +{ + "name": "try-playwright-scripts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "try-playwright-scripts", + "devDependencies": { + "typescript": "^7.0.2" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a5d44a --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "try-playwright-scripts", + "private": true, + "description": "Dependencies for repository-level scripts like update_pw.mjs", + "scripts": { + "update-pw": "node update_pw.mjs" + }, + "devDependencies": { + "typescript": "^7.0.2" + } +} diff --git a/update_pw.mjs b/update_pw.mjs index d1c525e..e6d0dbe 100755 --- a/update_pw.mjs +++ b/update_pw.mjs @@ -1,6 +1,12 @@ import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; +import { isBuiltin } from 'module'; +// The TypeScript 7 API is experimental and may change between releases. +import { API } from 'typescript/unstable/sync'; +import { createVirtualFileSystem } from 'typescript/unstable/fs'; +import { SyntaxKind } from 'typescript/unstable/ast'; +import { forEachLeadingCommentRange } from 'typescript/unstable/ast/scanner'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -40,103 +46,205 @@ async function getNpmFile(packageName, file) { } /** - * Removes import statements, reference directives, copyright headers, and export statements from the beginning of a type definition file. - * This prevents issues with relative imports that don't exist in the concatenated file. + * Parses the given declaration files with the TypeScript compiler and passes the + * resulting source files to `callback`. The files only live in a virtual file system. * - * @param {string} content - The file content to process - * @returns {string} - The content with imports/references/exports/copyright headers stripped + * @template T + * @param {Record} files - Map of file name to file content + * @param {(sourceFiles: Record) => T} callback + * @returns {T} */ -function stripFileHeader(content) { - const lines = content.split('\n'); - let startIndex = 0; - let inMultiLineComment = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - - // Check for multi-line comment start - if (line.startsWith('/**') || line.startsWith('/*')) { - inMultiLineComment = true; - startIndex = i + 1; - continue; - } +function withParsedFiles(files, callback) { + const root = '/virtual'; + const toFileName = (/** @type {string} */ name) => `${root}/${name}`; + const api = new API({ + cwd: root, + fs: createVirtualFileSystem(Object.fromEntries(Object.entries(files).map(([name, content]) => [toFileName(name), content]))), + }); + try { + const snapshot = api.updateSnapshot({ openFiles: Object.keys(files).map(toFileName) }); + const sourceFiles = Object.fromEntries(Object.keys(files).map(name => { + const fileName = toFileName(name); + const sourceFile = snapshot.getDefaultProjectForFile(fileName)?.program.getSourceFile(fileName); + if (!sourceFile) + throw new Error(`Could not parse ${name}`); + if (sourceFile.text !== files[name]) + throw new Error(`Parsed text of ${name} does not match its content`); + return [name, sourceFile]; + })); + return callback(sourceFiles); + } finally { + api.close(); + } +} - // Check for multi-line comment end - if (inMultiLineComment) { - if (line.endsWith('*/')) { - inMultiLineComment = false; - startIndex = i + 1; - } - continue; - } +/** + * @typedef {{ start: number, end: number, text: string }} TextEdit + */ - // Skip single-line comments - if (line.startsWith('//')) { - startIndex = i + 1; - continue; - } +/** + * @param {string} content + * @param {TextEdit[]} edits - Non-overlapping edits + * @returns {string} + */ +function applyEdits(content, edits) { + let result = content; + for (const { start, end, text } of [...edits].sort((a, b) => b.start - a.start)) + result = result.slice(0, start) + text + result.slice(end); + return result; +} - // Skip empty lines - if (line === '') { - startIndex = i + 1; - continue; - } +/** + * Guards against the experimental API reporting positions in another unit (e.g. UTF-8 bytes). + * + * @param {import('typescript/unstable/ast').SourceFile} sourceFile + * @param {import('typescript/unstable/ast').Node} node + * @param {string} expected + */ +function assertNodeStartsWith(sourceFile, node, expected) { + const actual = sourceFile.text.slice(node.getStart(sourceFile), node.getStart(sourceFile) + expected.length); + if (actual !== expected) + throw new Error(`Unexpected node position in ${sourceFile.fileName}: expected "${expected}", got "${actual}"`); +} - // Skip triple-slash reference directives - if (line.startsWith('///')) { - startIndex = i + 1; - continue; - } +/** + * Whether the module can't be resolved from within the concatenated file: relative + * paths point to files which got inlined and only the Node.js globals are available. + * + * @param {import('typescript/unstable/ast').Expression | undefined} moduleSpecifier + * @returns {boolean} + */ +function isUnresolvableModule(moduleSpecifier) { + if (moduleSpecifier?.kind !== SyntaxKind.StringLiteral) + return false; + return moduleSpecifier.text.startsWith('.') || isBuiltin(moduleSpecifier.text); +} - // Skip import statements - if (line.startsWith('import ') || line.startsWith('import{')) { - startIndex = i + 1; - continue; +/** + * Removes the module syntax from a type definition file so its declarations can be + * concatenated into an ambient module declaration: + * - imports/re-exports of relative paths and Node.js builtins (they don't exist in the concatenated file) + * - the file header (copyright comments and triple-slash reference directives) + * + * Imports of other packages (e.g. the optional `zod` import, guarded by `@ts-ignore`) are kept. + * + * @param {import('typescript/unstable/ast').SourceFile} sourceFile + * @returns {string} + */ +function stripModuleSyntax(sourceFile) { + /** @type {TextEdit[]} */ + const edits = []; + const isModuleSyntax = (/** @type {import('typescript/unstable/ast').Statement} */ statement) => { + switch (statement.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + return isUnresolvableModule(statement.moduleSpecifier); + case SyntaxKind.ImportEqualsDeclaration: + return statement.moduleReference.kind === SyntaxKind.ExternalModuleReference && isUnresolvableModule(statement.moduleReference.expression); + default: + return false; } + }; - // Skip export statements at the top (but not export type/interface declarations) - if (line.startsWith('export ') && !line.match(/^export (type|interface|class|const|function|namespace|declare)/)) { - startIndex = i + 1; + for (const statement of sourceFile.statements) { + if (!isModuleSyntax(statement)) continue; - } + assertNodeStartsWith(sourceFile, statement, statement.kind === SyntaxKind.ExportDeclaration ? 'export' : 'import'); + // The full range includes leading trivia, e.g. `// @ts-ignore` comments belonging to the statement. + edits.push({ start: statement.pos, end: statement.end, text: '' }); + } - // We've hit actual content, stop skipping - break; + // Drop the file header (copyright comments and triple-slash directives), but keep the + // comments directly attached to the first remaining statement, e.g. its JSDoc. + const firstStatement = sourceFile.statements.find(statement => !isModuleSyntax(statement)); + if (firstStatement) { + const { text } = sourceFile; + /** @type {{ pos: number, end: number }[]} */ + const comments = []; + forEachLeadingCommentRange(text, firstStatement.pos, (pos, end) => { + comments.push({ pos, end }); + }); + let headerEnd = firstStatement.getStart(sourceFile); + for (const comment of comments.reverse()) { + const isSeparatedByBlankLine = /\n[ \t\r]*\n/.test(text.slice(comment.end, headerEnd)); + if (isSeparatedByBlankLine || text.startsWith('///', comment.pos)) + break; + headerEnd = comment.pos; + } + edits.push({ start: firstStatement.pos, end: headerEnd, text: '' }); } - return lines.slice(startIndex).join('\n'); + return applyEdits(sourceFile.text, edits); +} + +/** + * Rewrites module specifiers of import/export declarations and import types. + * + * @param {import('typescript/unstable/ast').SourceFile} sourceFile + * @param {Record} replacements - Map of old to new module specifier + * @returns {string} + */ +function rewriteModuleSpecifiers(sourceFile, replacements) { + /** @type {TextEdit[]} */ + const edits = []; + const moduleSpecifierParents = new Set([ + SyntaxKind.ImportDeclaration, + SyntaxKind.ExportDeclaration, + SyntaxKind.ExternalModuleReference, + SyntaxKind.LiteralType, // import('...') types + ]); + const visit = (/** @type {import('typescript/unstable/ast').Node} */ node) => { + if (node.kind === SyntaxKind.StringLiteral && moduleSpecifierParents.has(node.parent.kind) && Object.hasOwn(replacements, node.text)) { + const start = node.getStart(sourceFile); + const quote = sourceFile.text[start]; + assertNodeStartsWith(sourceFile, node, quote + node.text + quote); + edits.push({ start, end: node.end, text: quote + replacements[node.text] + quote }); + } + node.forEachChild(visit); + }; + visit(sourceFile); + return applyEdits(sourceFile.text, edits); } async function updateFrontendTypes() { const typesFile = 'frontend/src/components/Editor/types.txt'; - let typesBuffer = ''; - - // Add Node.js global types - typesBuffer += stripFileHeader(await getNpmFile('@types/node@18', 'globals.d.ts')); - typesBuffer += '\n'; - - // Add playwright-core module - typesBuffer += 'declare module \'playwright-core\' {\n'; - typesBuffer += await getNpmFile(`playwright-core`, 'types/protocol.d.ts'); - typesBuffer += stripFileHeader(await getNpmFile(`playwright-core`, 'types/structs.d.ts')); - typesBuffer += '\n'; - typesBuffer += stripFileHeader(await getNpmFile(`playwright-core`, 'types/types.d.ts')); - typesBuffer += '}\n'; - - // Add playwright module (re-exports playwright-core) - typesBuffer += 'declare module \'playwright\' {\n'; - typesBuffer += ' export * from \'playwright-core\';\n'; - typesBuffer += '}\n'; - - // Add @playwright/test module - typesBuffer += 'declare module \'@playwright/test\' {\n'; - const testTypes = await getNpmFile('playwright', 'types/test.d.ts'); - // Fix internal reference paths that won't exist in the concatenated file - const fixedTestTypes = testTypes.split('\n') - .map(line => line.replace('@playwright/test/types/expect-types', '@playwright/test-expect')) - .join('\n'); - typesBuffer += fixedTestTypes; - typesBuffer += '}\n'; + const files = { + 'globals.d.ts': await getNpmFile('@types/node@18', 'globals.d.ts'), + 'protocol.d.ts': await getNpmFile(`playwright-core`, 'types/protocol.d.ts'), + 'structs.d.ts': await getNpmFile(`playwright-core`, 'types/structs.d.ts'), + 'types.d.ts': await getNpmFile(`playwright-core`, 'types/types.d.ts'), + 'test.d.ts': await getNpmFile('playwright', 'types/test.d.ts'), + }; + + const typesBuffer = withParsedFiles(files, sourceFiles => { + let typesBuffer = ''; + + // Add Node.js global types + typesBuffer += stripModuleSyntax(sourceFiles['globals.d.ts']); + typesBuffer += '\n'; + + // Add playwright-core module + typesBuffer += 'declare module \'playwright-core\' {\n'; + typesBuffer += files['protocol.d.ts']; + typesBuffer += stripModuleSyntax(sourceFiles['structs.d.ts']); + typesBuffer += '\n'; + typesBuffer += stripModuleSyntax(sourceFiles['types.d.ts']); + typesBuffer += '}\n'; + + // Add playwright module (re-exports playwright-core) + typesBuffer += 'declare module \'playwright\' {\n'; + typesBuffer += ' export * from \'playwright-core\';\n'; + typesBuffer += '}\n'; + + // Add @playwright/test module + typesBuffer += 'declare module \'@playwright/test\' {\n'; + // Fix internal reference paths that won't exist in the concatenated file + typesBuffer += rewriteModuleSpecifiers(sourceFiles['test.d.ts'], { + '@playwright/test/types/expect-types': '@playwright/test-expect', + }); + typesBuffer += '}\n'; + return typesBuffer; + }); fs.writeFileSync(typesFile, typesBuffer); } From d9d87e98d3843aa382cf4e084041952410dbdfdc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 8 Oct 2026 14:34:59 -0700 Subject: [PATCH 2/2] chore: regenerate editor types with the parser-based update script Generated from playwright-core/playwright 1.64.0. Compared to the line-based script this keeps the JSDoc of `Serializable` and the `URLPattern` comment. Co-Authored-By: Claude Opus 5.5 --- frontend/src/components/Editor/types.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/components/Editor/types.txt b/frontend/src/components/Editor/types.txt index 38abcad..e8b071b 100644 --- a/frontend/src/components/Editor/types.txt +++ b/frontend/src/components/Editor/types.txt @@ -24946,6 +24946,9 @@ Error was thrown. "Schema.getDomains": Schema.getDomainsReturnValue; } } +/** + * Can be converted to JSON + */ export type Serializable = any; /** * Can be converted to JSON, but may also contain JSHandles. @@ -24978,6 +24981,8 @@ export type SmartHandle = [T] extends [Node] ? ElementHandle : JSHandle export type ElementHandleForTag = ElementHandle; export type BindingSource = { context: BrowserContext, page: Page, frame: Frame }; +// Use the global URLPattern type if available (Node.js 22+, modern browsers), +// otherwise fall back to `never` so it disappears from union types. type URLPattern = typeof globalThis extends { URLPattern: new (...args: any) => infer T } ? T : never; type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {