UNPKG

chrome-devtools-frontend

Version:
231 lines (206 loc) 6.61 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); const cwd = process.cwd(); const frontEndDir = path.join(cwd, 'front_end'); const testsDir = path.join(cwd, 'test'); const env = process.env; const extractArgument = argName => { const arg = process.argv.find(value => value.startsWith(`${argName}`)); if (!arg) { return; } return arg.slice(`${argName}=`.length); }; const relativeFileName = absoluteName => { return path.relative(cwd, absoluteName); }; const currentTimeString = () => { return new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); }; const NODE_PATH = path.join('third_party', 'node', 'node.py'); const ESBUILD_PATH = path.join('third_party', 'esbuild', 'esbuild'); const GENERATE_CSS_JS_FILES_PATH = path.join( 'scripts', 'build', 'generate_css_js_files.js', ); let tId = -1; // Extract the target if it's provided. const target = extractArgument('--target') || 'Default'; const TARGET_GEN_DIR = path.join('out', target, 'gen'); // Make sure that the target has // - `is_debug = true` // - `devtools_skip_typecheck = true` // flags set. const assertTargetArgsForWatchBuild = async () => { const {status, stdout} = childProcess.spawnSync( 'gn', ['args', '--list', '--json', `out/${target}`], {cwd, env, stdio: 'pipe'}, ); const stdoutText = stdout.toString(); if (status !== 0) { throw new Error( `gen args --list --json failed for target ${target}\n${stdoutText}`, ); } let args; try { args = JSON.parse(stdoutText); } catch (err) { if (stdoutText.includes('devtools_css_hot_reload_enabled')) { console.error( '\n❗❗ You must remove `devtools_css_hot_reload_enabled` from your args.gn.\n', ); } throw new Error(`Parsing args of target ${target} is failed\n${err}`, { cause: err, }); } const argsMap = Object.fromEntries(args.map(arg => [arg.name, arg])); const assertTrueArg = argName => { const argString = argsMap[argName].current ? argsMap[argName].current.value : argsMap[argName].default.value; if (argString !== 'true') { throw new Error( `${argName} is expected to be 'true' but it is '${argString}' in target ${target}`, ); } }; try { assertTrueArg('is_debug'); assertTrueArg('devtools_skip_typecheck'); } catch (err) { console.error( `watch_build needs is_debug and devtools_skip_typecheck args to be set to true for target ${target}\n${ err?.message}\n`, ); process.exit(1); } }; const runGenerateCssFiles = ({fileName}) => { const scriptArgs = [ /* buildTimestamp */ Date.now(), /* isDebugString */ 'true', /* targetName */ target, /* srcDir */ '', /* targetGenDir */ TARGET_GEN_DIR, /* files */ relativeFileName(fileName), ]; childProcess.spawnSync( 'vpython3', [NODE_PATH, '--output', GENERATE_CSS_JS_FILES_PATH, ...scriptArgs], {cwd, env, stdio: 'inherit'}, ); }; const changedFiles = new Set(); const onFileChange = async fileName => { changedFiles.add(fileName); // Debounce to handle them in batch. // At 250ms, we're optimizing for individual file changes. // On branch changes, its possible a ninja rebuild may start before the checkout is complete, but it will likely quickly error out. Either way, another rebuild will be attempted immediately after. clearTimeout(tId); tId = setTimeout(buildFiles, 250); }; const buildFiles = async () => { // If we need a ninja rebuild, do that and quit const nonTSOrCSSFileNames = Array.from(changedFiles) .filter( f => !f.endsWith('.css') && !f.endsWith('.ts'), ); if (nonTSOrCSSFileNames.length) { console.log( `${currentTimeString()} - ${ nonTSOrCSSFileNames.map( relativeFileName, )} changed, running ninja`, ); changedFiles.clear(); childProcess.spawnSync('autoninja', ['-C', `out/${target}`], { cwd, env, stdio: 'inherit', }); return; } // …Otherwise we can do fast rebuilds changedFiles.forEach(fastRebuildFile); console.assert( changedFiles.size === 0, `⚠️⚠️⚠️ Some changed files NOT built: ${Array.from(changedFiles.values())}`, ); }; const fastRebuildFile = async fileName => { if (fileName.endsWith('.css')) { console.log( `${currentTimeString()} - ${ relativeFileName( fileName, )} changed, notifying frontend`, ); runGenerateCssFiles({fileName: relativeFileName(fileName)}); changedFiles.delete(fileName); return; } if (fileName.endsWith('.ts')) { console.log( `${currentTimeString()} - ${ relativeFileName( fileName, )} changed, generating js file`, ); const jsFileName = `${fileName.substring(0, fileName.length - 3)}.js`; const outFile = path.resolve( 'out', target, 'gen', relativeFileName(jsFileName), ); // Hack to mimic node_ts_library for test files. const cjsForTests = fileName.includes('/test/') ? ['--format=cjs'] : []; changedFiles.delete(fileName); const res = childProcess.spawnSync( ESBUILD_PATH, [ fileName, `--outfile=${outFile}`, '--sourcemap', ...cjsForTests, ], {cwd, env, stdio: 'inherit'}, ); if (res?.status === 1) { console.warn( `TS compilation failed for \x1B[1m${path.relative(cwd, fileName)}\x1B`, ); } return; } }; console.log('Running initial build before watching changes'); assertTargetArgsForWatchBuild(); childProcess.spawnSync('autoninja', ['-C', `out/${target}`], { cwd, env, stdio: 'inherit', }); // Watch the front_end and test folder and build on any change. console.log(`Watching for changes in ${frontEndDir} and ${testsDir}`); fs.watch(frontEndDir, {recursive: true}) .on( 'change', (_, fileName) => onFileChange(path.join(frontEndDir, fileName)), ); fs.watch(testsDir, {recursive: true}) .on( 'change', (_, fileName) => onFileChange(path.join(testsDir, fileName)), );