chore: migrate to ESM (#303)

- [Why do I need `.js`
extension?](https://stackoverflow.com/a/77150985/6512681)
- [Why setting `rootDir` in the
`tsconfig.json`?](https://stackoverflow.com/a/58941798/6512681)
- [How to ensure that we add the `.js` extension via
ESLint](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/extensions.md#importextensions)

Fixes https://github.com/microsoft/playwright-mcp/issues/302
This commit is contained in:
Max Schmitt 2025-04-30 23:06:56 +02:00 committed by GitHub
parent 878be97668
commit 685dea9e19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 121 additions and 101 deletions

2
cli.js
View File

@ -15,4 +15,4 @@
* limitations under the License.
*/
require('./lib/program');
import './lib/program.js';

View File

@ -33,6 +33,7 @@ const plugins = {
};
export const baseRules = {
"import/extensions": ["error", "ignorePackages", {ts: "always"}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-unused-vars": [
2,

View File

@ -15,5 +15,5 @@
* limitations under the License.
*/
const { createServer } = require('./lib/index');
module.exports = { createServer };
import { createServer } from './lib/index';
export default { createServer };

2
package-lock.json generated
View File

@ -16,7 +16,7 @@
"zod-to-json-schema": "^3.24.4"
},
"bin": {
"mcp-server-playwright": "cli.js"
"mcp-server-playwright": "cli.mjs"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@ -2,6 +2,7 @@
"name": "@playwright/mcp",
"version": "0.0.18",
"description": "Playwright Tools for MCP",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/playwright-mcp.git"

View File

@ -20,9 +20,9 @@ import os from 'os';
import path from 'path';
import { devices } from 'playwright';
import { sanitizeForFilePath } from './tools/utils';
import { sanitizeForFilePath } from './tools/utils.js';
import type { Config, ToolCapability } from '../config';
import type { Config, ToolCapability } from '../config.js';
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
export type CLIOptions = {

View File

@ -16,13 +16,13 @@
import * as playwright from 'playwright';
import { waitForCompletion } from './tools/utils';
import { ManualPromise } from './manualPromise';
import { Tab } from './tab';
import { waitForCompletion } from './tools/utils.js';
import { ManualPromise } from './manualPromise.js';
import { Tab } from './tab.js';
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
import type { ModalState, Tool, ToolActionResult } from './tools/tool';
import type { Config } from '../config';
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
import type { Config } from '../config.js';
type PendingAction = {
dialogShown: ManualPromise<void>;

View File

@ -14,22 +14,22 @@
* limitations under the License.
*/
import { createServerWithTools } from './server';
import common from './tools/common';
import console from './tools/console';
import dialogs from './tools/dialogs';
import files from './tools/files';
import install from './tools/install';
import keyboard from './tools/keyboard';
import navigate from './tools/navigate';
import network from './tools/network';
import pdf from './tools/pdf';
import snapshot from './tools/snapshot';
import tabs from './tools/tabs';
import screen from './tools/screen';
import { createServerWithTools } from './server.js';
import common from './tools/common.js';
import console from './tools/console.js';
import dialogs from './tools/dialogs.js';
import files from './tools/files.js';
import install from './tools/install.js';
import keyboard from './tools/keyboard.js';
import navigate from './tools/navigate.js';
import network from './tools/network.js';
import pdf from './tools/pdf.js';
import snapshot from './tools/snapshot.js';
import tabs from './tools/tabs.js';
import screen from './tools/screen.js';
import type { Tool } from './tools/tool';
import type { Config } from '../config';
import type { Tool } from './tools/tool.js';
import type { Config } from '../config.js';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
const snapshotTools: Tool<any>[] = [
@ -60,7 +60,7 @@ const screenshotTools: Tool<any>[] = [
...tabs(false),
];
const packageJSON = require('../package.json');
import packageJSON from '../package.json' with { type: 'json' };
export async function createServer(config: Config = {}): Promise<Server> {
const allTools = config.vision ? screenshotTools : snapshotTools;

View File

@ -16,14 +16,14 @@
import { program } from 'commander';
import { createServer } from './index';
import { ServerList } from './server';
import { createServer } from './index.js';
import { ServerList } from './server.js';
import { startHttpTransport, startStdioTransport } from './transport';
import { startHttpTransport, startStdioTransport } from './transport.js';
import { resolveConfig } from './config';
import { resolveConfig } from './config.js';
const packageJSON = require('../package.json');
import packageJSON from '../package.json' with { type: 'json' };
program
.version('Version ' + packageJSON.version)

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Context } from '../context';
import type { Context } from '../context.js';
export type ResourceSchema = {
uri: string;

View File

@ -18,10 +18,10 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { Context } from './context';
import { Context } from './context.js';
import type { Tool } from './tools/tool';
import type { Config } from '../config';
import type { Tool } from './tools/tool.js';
import type { Config } from '../config.js';
type MCPServerOptions = {
name: string;

View File

@ -16,9 +16,9 @@
import * as playwright from 'playwright';
import { PageSnapshot } from './pageSnapshot';
import { PageSnapshot } from './pageSnapshot.js';
import type { Context } from './context';
import type { Context } from './context.js';
export class Tab {
readonly context: Context;

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const wait: ToolFactory = captureSnapshot => defineTool({
capability: 'wait',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool } from './tool';
import { defineTool } from './tool.js';
const console = defineTool({
capability: 'core',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const handleDialog: ToolFactory = captureSnapshot => defineTool({
capability: 'core',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const uploadFile: ToolFactory = captureSnapshot => defineTool({
capability: 'files',

View File

@ -18,7 +18,7 @@ import { fork } from 'child_process';
import path from 'path';
import { z } from 'zod';
import { defineTool } from './tool';
import { defineTool } from './tool.js';
const install = defineTool({
capability: 'install',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const pressKey: ToolFactory = captureSnapshot => defineTool({
capability: 'core',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const navigate: ToolFactory = captureSnapshot => defineTool({
capability: 'core',

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool } from './tool';
import { defineTool } from './tool.js';
import type * as playwright from 'playwright';

View File

@ -15,10 +15,10 @@
*/
import { z } from 'zod';
import { defineTool } from './tool';
import { defineTool } from './tool.js';
import * as javascript from '../javascript';
import { outputFile } from '../config';
import * as javascript from '../javascript.js';
import { outputFile } from '../config.js';
const pdf = defineTool({
capability: 'pdf',

View File

@ -15,9 +15,9 @@
*/
import { z } from 'zod';
import { defineTool } from './tool';
import { defineTool } from './tool.js';
import * as javascript from '../javascript';
import * as javascript from '../javascript.js';
const elementSchema = z.object({
element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),

View File

@ -16,9 +16,9 @@
import { z } from 'zod';
import { defineTool } from './tool';
import * as javascript from '../javascript';
import { outputFile } from '../config';
import { defineTool } from './tool.js';
import * as javascript from '../javascript.js';
import { outputFile } from '../config.js';
import type * as playwright from 'playwright';

View File

@ -15,7 +15,7 @@
*/
import { z } from 'zod';
import { defineTool, type ToolFactory } from './tool';
import { defineTool, type ToolFactory } from './tool.js';
const listTabs = defineTool({
capability: 'tabs',

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
import type { z } from 'zod';
import type { Context } from '../context';
import type { Context } from '../context.js';
import type * as playwright from 'playwright';
import type { ToolCapability } from '../../config';
import type { ToolCapability } from '../../config.js';
export type ToolSchema<Input extends InputType> = {
name: string;

View File

@ -15,7 +15,7 @@
*/
import type * as playwright from 'playwright';
import type { Context } from '../context';
import type { Context } from '../context.js';
export async function waitForCompletion<R>(context: Context, page: playwright.Page, callback: () => Promise<R>): Promise<R> {
const requests = new Set<playwright.Request>();

View File

@ -18,7 +18,7 @@ import http from 'node:http';
import assert from 'node:assert';
import crypto from 'node:crypto';
import { ServerList } from './server';
import { ServerList } from './server.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('test snapshot tool list', async ({ client }) => {
const { tools } = await client.listTools();

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('cdp server', async ({ cdpEndpoint, startClient }) => {
const client = await startClient({ args: [`--cdp-endpoint=${cdpEndpoint}`] });

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('browser_console_messages', async ({ client }) => {
await client.callTool({

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('browser_navigate', async ({ client }) => {
expect(await client.callTool({

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('browser_take_screenshot (viewport)', async ({ startClient, server }) => {
const client = await startClient({

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
// https://github.com/microsoft/playwright/issues/35663
test.skip(({ mcpBrowser, mcpHeadless }) => mcpBrowser === 'webkit' && mcpHeadless);

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
import fs from 'fs/promises';
test('browser_file_upload', async ({ client }) => {

View File

@ -15,6 +15,7 @@
*/
import fs from 'fs';
import url from 'url';
import path from 'path';
import { chromium } from 'playwright';
@ -22,7 +23,7 @@ import { test as baseTest, expect as baseExpect } from '@playwright/test';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { spawn } from 'child_process';
import { TestServer } from './testserver';
import { TestServer } from './testserver/index.ts';
import type { Config } from '../config';
@ -69,9 +70,11 @@ export const test = baseTest.extend<TestFixtures, WorkerFixtures>({
await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
args.push(`--config=${configFile}`);
}
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
const transport = new StdioClientTransport({
command: 'node',
args: [path.join(__dirname, '../cli.js'), ...args],
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
});
client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
for (const mcpHeadless of [false, true]) {
test.describe(`mcpHeadless: ${mcpHeadless}`, () => {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('stitched aria frames', async ({ client }) => {
expect(await client.callTool({

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('test reopen browser', async ({ client }) => {
await client.callTool({

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('browser_network_requests', async ({ client, server }) => {
server.route('/', (req, res) => {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('save as pdf unavailable', async ({ startClient }) => {
const client = await startClient({ args: ['--caps="no-pdf"'] });

View File

@ -16,7 +16,7 @@
import fs from 'fs';
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('browser_take_screenshot (viewport)', async ({ client }) => {
expect(await client.callTool({

View File

@ -14,14 +14,18 @@
* limitations under the License.
*/
import url from 'node:url';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { test as baseTest } from './fixtures';
import { test as baseTest } from './fixtures.js';
import { expect } from 'playwright/test';
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
const test = baseTest.extend<{ serverEndpoint: string }>({
serverEndpoint: async ({}, use) => {
const cp = spawn('node', [path.join(__dirname, '../cli.js'), '--port', '0'], { stdio: 'pipe' });
const cp = spawn('node', [path.join(path.dirname(__filename), '../cli.js'), '--port', '0'], { stdio: 'pipe' });
try {
let stdout = '';
const url = await new Promise<string>(resolve => cp.stdout?.on('data', data => {

View File

@ -16,7 +16,7 @@
import { chromium } from 'playwright';
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

View File

@ -16,13 +16,18 @@
*/
import fs from 'fs';
import url from 'node:url';
import http from 'http';
import https from 'https';
import path from 'path';
import debug from 'debug';
const fulfillSymbol = Symbol('fulfil callback');
const rejectSymbol = Symbol('reject callback');
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
export class TestServer {
private _server: http.Server;
readonly debugServer: any;
@ -42,8 +47,8 @@ export class TestServer {
static async createHTTPS(port: number): Promise<TestServer> {
const server = new TestServer(port, {
key: await fs.promises.readFile(path.join(__dirname, 'key.pem')),
cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')),
key: await fs.promises.readFile(path.join(path.dirname(__filename), 'key.pem')),
cert: await fs.promises.readFile(path.join(path.dirname(__filename), 'cert.pem')),
passphrase: 'aaaa',
});
await new Promise(x => server._server.once('listening', x));
@ -56,7 +61,7 @@ export class TestServer {
else
this._server = http.createServer(this._onRequest.bind(this));
this._server.listen(port);
this.debugServer = require('debug')('pw:testserver');
this.debugServer = debug('pw:testserver');
const cross_origin = '127.0.0.1';
const same_origin = 'localhost';

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './fixtures';
import { test, expect } from './fixtures.js';
test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => {
test.skip(mcpBrowser === 'firefox');

View File

@ -3,10 +3,12 @@
"target": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "node",
"moduleResolution": "nodenext",
"strict": true,
"module": "CommonJS",
"outDir": "./lib"
"module": "NodeNext",
"rootDir": "src",
"outDir": "./lib",
"resolveJsonModule": true
},
"include": [
"src",

View File

@ -16,21 +16,22 @@
*/
// @ts-check
const fs = require('node:fs');
const path = require('node:path');
const zodToJsonSchema = require('zod-to-json-schema').default;
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
import zodToJsonSchema from 'zod-to-json-schema'
const commonTools = require('../lib/tools/common').default;
const consoleTools = require('../lib/tools/console').default;
const dialogsTools = require('../lib/tools/dialogs').default;
const filesTools = require('../lib/tools/files').default;
const installTools = require('../lib/tools/install').default;
const keyboardTools = require('../lib/tools/keyboard').default;
const navigateTools = require('../lib/tools/navigate').default;
const pdfTools = require('../lib/tools/pdf').default;
const snapshotTools = require('../lib/tools/snapshot').default;
const tabsTools = require('../lib/tools/tabs').default;
const screenTools = require('../lib/tools/screen').default;
import commonTools from '../lib/tools/common.js';
import consoleTools from '../lib/tools/console.js';
import dialogsTools from '../lib/tools/dialogs.js';
import filesTools from '../lib/tools/files.js';
import installTools from '../lib/tools/install.js';
import keyboardTools from '../lib/tools/keyboard.js';
import navigateTools from '../lib/tools/navigate.js';
import pdfTools from '../lib/tools/pdf.js';
import snapshotTools from '../lib/tools/snapshot.js';
import tabsTools from '../lib/tools/tabs.js';
import screenTools from '../lib/tools/screen.js';
// Category definitions for tools
const categories = {
@ -63,6 +64,9 @@ const categories = {
],
};
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
const kStartMarker = `<!--- Generated by ${path.basename(__filename)} -->`;
const kEndMarker = `<!--- End of generated section -->`;
@ -152,7 +156,7 @@ async function updateReadme() {
}
}
const readmePath = path.join(__dirname, '..', 'README.md');
const readmePath = path.join(path.dirname(__filename), '..', 'README.md');
const readmeContent = await fs.promises.readFile(readmePath, 'utf-8');
const startMarker = readmeContent.indexOf(kStartMarker);
const endMarker = readmeContent.indexOf(kEndMarker);