File size: 4,641 Bytes
52efc7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import esbuild from 'esbuild';
import fs from 'node:fs'; // Import the full fs module
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const manifestPath = path.resolve(
  __dirname,
  '../src/agents/browser/browser-tools-manifest.json',
);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));

// Only exclude tools explicitly mentioned in the manifest's exclude list
const excludedToolsFiles = (manifest.exclude || []).map((t) => t.name);

// Basic esbuild plugin to empty out excluded modules
const emptyModulePlugin = {
  name: 'empty-modules',
  setup(build) {
    if (excludedToolsFiles.length === 0) return;

    // Create a filter that matches any of the excluded tools
    const excludeFilter = new RegExp(`(${excludedToolsFiles.join('|')})\\.js$`);

    build.onResolve({ filter: excludeFilter }, (args) => {
      // Check if we are inside a tools directory to avoid accidental matches
      if (
        args.importer.includes('chrome-devtools-mcp') &&
        /[\\/]tools[\\/]/.test(args.importer)
      ) {
        return { path: args.path, namespace: 'empty' };
      }
      return null;
    });

    build.onLoad({ filter: /.*/, namespace: 'empty' }, (_args) => ({
      contents: 'export {};', // Empty module (ESM)
      loader: 'js',
    }));
  },
};

async function bundle() {
  try {
    const entryPoint = path.resolve(
      __dirname,
      '../../../node_modules/chrome-devtools-mcp/build/src/index.js',
    );
    await esbuild.build({
      entryPoints: [entryPoint],
      bundle: true,
      outfile: path.resolve(
        __dirname,
        '../dist/bundled/chrome-devtools-mcp.mjs',
      ),
      format: 'esm',
      platform: 'node',
      plugins: [emptyModulePlugin],
      external: [
        'puppeteer-core',
        '/bundled/*',
        '../../../node_modules/puppeteer-core/*',
      ],
      banner: {
        js: 'import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);',
      },
    });

    // Copy third_party assets
    const srcThirdParty = path.resolve(
      __dirname,
      '../../../node_modules/chrome-devtools-mcp/build/src/third_party',
    );
    const destThirdParty = path.resolve(
      __dirname,
      '../dist/bundled/third_party',
    );

    if (fs.existsSync(srcThirdParty)) {
      if (fs.existsSync(destThirdParty)) {
        fs.rmSync(destThirdParty, { recursive: true, force: true });
      }
      fs.cpSync(srcThirdParty, destThirdParty, {
        recursive: true,
        filter: (src) => {
          // Skip large/unnecessary bundles that are either explicitly excluded
          // or not required for the browser agent functionality.
          return (
            !src.includes('lighthouse-devtools-mcp-bundle.js') &&
            !src.includes('devtools-formatter-worker.js')
          );
        },
      });
    } else {
      console.warn(`Warning: third_party assets not found at ${srcThirdParty}`);
    }

    // Sanitize any hardcoded sensitive API keys in the bundled and copied output files.
    // Replace the plaintext Google CrUX API key with dynamic process.env lookup at runtime.
    const bundleMcpFile = path.resolve(
      __dirname,
      '../dist/bundled/chrome-devtools-mcp.mjs',
    );
    const destThirdPartyIndex = path.resolve(
      __dirname,
      '../dist/bundled/third_party/index.js',
    );

    const sanitizeFile = (filePath) => {
      if (fs.existsSync(filePath)) {
        const content = fs.readFileSync(filePath, 'utf8');
        const keyPattern = /AIzaSy[A-Za-z0-9_-]{30,40}/;
        if (keyPattern.test(content)) {
          console.log(`Sanitizing hardcoded API key in: ${filePath}`);
          // Replace single-quoted, double-quoted, and backtick occurrences of any matching Google API key with process.env lookup
          const quotedPattern = /([\x27\x22\x60])AIzaSy[A-Za-z0-9_-]{30,40}\1/g;
          const sanitizedContent = content.replace(
            quotedPattern,
            '(process.env.CRUX_API_KEY || "")',
          );

          if (keyPattern.test(sanitizedContent)) {
            throw new Error(
              `Failed to fully sanitize API key in ${filePath}. The key pattern was detected but could not be safely replaced.`,
            );
          }

          fs.writeFileSync(filePath, sanitizedContent, 'utf8');
        }
      }
    };

    sanitizeFile(bundleMcpFile);
    sanitizeFile(destThirdPartyIndex);
  } catch (error) {
    console.error('Error bundling chrome-devtools-mcp:', error);
    process.exit(1);
  }
}

bundle();