| // 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. |
| |
| // Run like the following: |
| // $ node scripts/build/cross_reference_ninja_and_tsc.js [Target] [Path to bundle], |
| // e.g. node scripts/build/cross_reference_ninja_and_tsc.js Default front_end/common:bundle, |
| |
| const fs = require('fs'); |
| const path = require('path'); |
| const childProcess = require('child_process'); |
| const [, , buildDir, gnTarget] = process.argv; |
| const cwd = process.cwd(); |
| |
| /** |
| * Execs a command. |
| * |
| * @param {string} cmd |
| * @return {string} |
| */ |
| async function exec(cmd) { |
| const env = process.env; |
| |
| return new Promise((resolve, reject) => { |
| const [util, ...args] = cmd.split(' '); |
| |
| let response = ''; |
| const process = childProcess.spawn(util, args, {cwd, env}); |
| process.stdout.on('data', data => { |
| response += data.toString(); |
| }); |
| |
| process.on('close', () => { |
| resolve(response); |
| }); |
| |
| process.on('error', err => { |
| reject(err); |
| }); |
| }); |
| } |
| |
| /** |
| * |
| * @param {string} buildDir |
| * @param {string} gnTarget |
| */ |
| async function buildTargetInfo(buildDir, gnTarget) { |
| /** |
| * @param {string} outputList |
| */ |
| const flattenOutput = outputList => { |
| try { |
| const outputFiles = JSON.parse(outputList); |
| |
| /** |
| * @type string[] |
| */ |
| return Object.values(outputFiles).reduce((prev, {outputs}) => { |
| if (!outputs) { |
| return prev; |
| } |
| |
| prev.push(...outputs); |
| return prev; |
| }, []); |
| } catch (e) { |
| return []; |
| } |
| }; |
| |
| // Grab the outputs of the build target itself. |
| const output = await exec(`gn desc out/${buildDir} ${gnTarget} outputs --format=json`); |
| if (output.startsWith('ERROR')) { |
| console.error('GN error:'); |
| console.error(output); |
| process.exit(1); |
| } |
| |
| const gnTargetOutputFileList = flattenOutput(output); |
| |
| // The response from this is a new line-separated list of targets. |
| const deps = await exec(`gn desc out/${buildDir} //${gnTarget} deps --all`); |
| const depsOutputFileList = deps.split('\n').map(async line => { |
| if (line.trim() === '') { |
| return []; |
| } |
| |
| const depOutputList = await exec(`gn desc out/${buildDir} ${line} outputs --format=json`); |
| return flattenOutput(depOutputList); |
| }); |
| |
| const fileList = await Promise.all(depsOutputFileList); |
| return [gnTargetOutputFileList, fileList] |
| .flat(Infinity) |
| // Only include those in gen. |
| .filter(file => file.startsWith(`//out/${buildDir}/gen/`)) |
| // Strip the build dir out. |
| .map(file => file.replace(`//out/${buildDir}/gen/`, '')); |
| } |
| |
| (async function init() { |
| // Go from foo:bundle to foo, on the basis that foo.ts / foo.js will be the entrypoint file. |
| const assumedName = path.basename(gnTarget).replace(/:.*$/, ''); |
| const assumedDir = gnTarget.replace(/:.*$/, ''); |
| const entryPointTs = path.join(cwd, assumedDir, `${assumedName}.ts`); |
| const entryPointJs = path.join(cwd, assumedDir, `${assumedName}.js`); |
| let entryPoint = entryPointTs; |
| |
| if (!fs.existsSync(entryPoint)) { |
| entryPoint = entryPointJs; |
| if (!fs.existsSync(entryPoint)) { |
| console.error(`Neither ${entryPointTs} nor ${entryPointJs} exists.`); |
| process.exit(1); |
| } |
| } |
| |
| console.log(`Output Build Dir: ${buildDir}`); |
| console.log(`GN Target: //${gnTarget}`); |
| console.log(`TS/JS Entry Point: ${entryPoint}`); |
| |
| try { |
| // Ask TypeScript to enumerate the files it knows about. |
| const types = path.join(cwd, 'front_end/global_typings'); |
| const tscPath = path.join(cwd, 'node_modules', '.bin', 'tsc'); |
| const tscOut = await exec(`${tscPath} ${entryPoint} --types ${types}/request_idle_callback.d.ts --types ${ |
| types}/global_defs.d.ts --types ${ |
| types}/intl_display_names.d.ts --noEmit --listFiles --allowJs --target esnext`); |
| |
| // Filter the list and remap to those that are explicitly in the front_end, excluding the entrypoint itself. |
| const frontEndFiles = |
| tscOut.split('\n') |
| .filter(line => line.includes('front_end') && line !== entryPoint && !line.includes('global_typings')) |
| // Ensure we look for the original file, not the .d.ts. |
| .map(line => line.replace(/\.d\.ts$/, '')) |
| // Ensure that any file that ends in .ts is replaced as the equivalent outputted .js file. |
| .map(line => line.replace(/\.ts$/, '.js')) |
| // Trim the files so that the path starts with front_end |
| .map(line => line.replace(/.*?front_end/, 'front_end')) |
| // Finally remove any files where stripping the .d.ts has resulted in a file with no suffix. |
| .filter(line => path.extname(line) !== ''); |
| |
| // Ask gn/ninja to do the same. |
| const gnFiles = await buildTargetInfo(buildDir, gnTarget); |
| |
| // Then cross reference. |
| const missingFiles = []; |
| for (const file of frontEndFiles) { |
| if (!gnFiles.includes(file)) { |
| missingFiles.push(file); |
| } |
| } |
| |
| if (missingFiles.length) { |
| console.error('TypeScript indicates it expects the following files:'); |
| console.error(missingFiles.map(file => ` - ${file}`).join('\n')); |
| console.error(`There ${ |
| missingFiles.length === 1 ? 'is 1 file' : |
| `are ${missingFiles.length} files`} not listed in the BUILD.gn as dependencies`); |
| console.error('Have you added all dependencies to the BUILD.gn?'); |
| process.exit(1); |
| } else { |
| console.log('No mismatches found'); |
| process.exit(0); |
| } |
| } catch (e) { |
| console.error(e); |
| process.exit(1); |
| } |
| })(); |