Paul Lewis | a1448c2 | 2020-07-23 12:22:04 | [diff] [blame] | 1 | // Copyright 2020 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | // Run like the following: |
| 6 | // $ node scripts/build/cross_reference_ninja_and_tsc.js [Target] [Path to bundle], |
| 7 | // e.g. node scripts/build/cross_reference_ninja_and_tsc.js Default front_end/common:bundle, |
| 8 | |
| 9 | const fs = require('fs'); |
| 10 | const path = require('path'); |
| 11 | const childProcess = require('child_process'); |
| 12 | const [, , buildDir, gnTarget] = process.argv; |
| 13 | const cwd = process.cwd(); |
| 14 | |
| 15 | /** |
| 16 | * Execs a command. |
| 17 | * |
| 18 | * @param {string} cmd |
| 19 | * @return {string} |
| 20 | */ |
| 21 | async function exec(cmd) { |
| 22 | const env = process.env; |
| 23 | |
| 24 | return new Promise((resolve, reject) => { |
| 25 | const [util, ...args] = cmd.split(' '); |
| 26 | |
| 27 | let response = ''; |
| 28 | const process = childProcess.spawn(util, args, {cwd, env}); |
| 29 | process.stdout.on('data', data => { |
| 30 | response += data.toString(); |
| 31 | }); |
| 32 | |
| 33 | process.on('close', () => { |
| 34 | resolve(response); |
| 35 | }); |
| 36 | |
| 37 | process.on('error', err => { |
| 38 | reject(err); |
| 39 | }); |
| 40 | }); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * |
| 45 | * @param {string} buildDir |
| 46 | * @param {string} gnTarget |
| 47 | */ |
| 48 | async function buildTargetInfo(buildDir, gnTarget) { |
| 49 | /** |
| 50 | * @param {string} outputList |
| 51 | */ |
| 52 | const flattenOutput = outputList => { |
| 53 | try { |
| 54 | const outputFiles = JSON.parse(outputList); |
| 55 | |
| 56 | /** |
| 57 | * @type string[] |
| 58 | */ |
| 59 | return Object.values(outputFiles).reduce((prev, {outputs}) => { |
| 60 | if (!outputs) { |
| 61 | return prev; |
| 62 | } |
| 63 | |
| 64 | prev.push(...outputs); |
| 65 | return prev; |
| 66 | }, []); |
| 67 | } catch (e) { |
| 68 | return []; |
| 69 | } |
| 70 | }; |
| 71 | |
| 72 | // Grab the outputs of the build target itself. |
| 73 | const output = await exec(`gn desc out/${buildDir} ${gnTarget} outputs --format=json`); |
Jack Franklin | 7a25be0 | 2020-07-24 08:54:12 | [diff] [blame] | 74 | if (output.startsWith('ERROR')) { |
| 75 | console.error('GN error:'); |
| 76 | console.error(output); |
| 77 | process.exit(1); |
| 78 | } |
| 79 | |
Paul Lewis | a1448c2 | 2020-07-23 12:22:04 | [diff] [blame] | 80 | const gnTargetOutputFileList = flattenOutput(output); |
| 81 | |
| 82 | // The response from this is a new line-separated list of targets. |
| 83 | const deps = await exec(`gn desc out/${buildDir} //${gnTarget} deps --all`); |
| 84 | const depsOutputFileList = deps.split('\n').map(async line => { |
| 85 | if (line.trim() === '') { |
| 86 | return []; |
| 87 | } |
| 88 | |
| 89 | const depOutputList = await exec(`gn desc out/${buildDir} ${line} outputs --format=json`); |
| 90 | return flattenOutput(depOutputList); |
| 91 | }); |
| 92 | |
| 93 | const fileList = await Promise.all(depsOutputFileList); |
| 94 | return [gnTargetOutputFileList, fileList] |
| 95 | .flat(Infinity) |
| 96 | // Only include those in gen. |
| 97 | .filter(file => file.startsWith(`//out/${buildDir}/gen/`)) |
| 98 | // Strip the build dir out. |
| 99 | .map(file => file.replace(`//out/${buildDir}/gen/`, '')); |
| 100 | } |
| 101 | |
| 102 | (async function init() { |
| 103 | // Go from foo:bundle to foo, on the basis that foo.ts / foo.js will be the entrypoint file. |
| 104 | const assumedName = path.basename(gnTarget).replace(/:.*$/, ''); |
| 105 | const assumedDir = gnTarget.replace(/:.*$/, ''); |
| 106 | const entryPointTs = path.join(cwd, assumedDir, `${assumedName}.ts`); |
| 107 | const entryPointJs = path.join(cwd, assumedDir, `${assumedName}.js`); |
| 108 | let entryPoint = entryPointTs; |
| 109 | |
| 110 | if (!fs.existsSync(entryPoint)) { |
| 111 | entryPoint = entryPointJs; |
| 112 | if (!fs.existsSync(entryPoint)) { |
| 113 | console.error(`Neither ${entryPointTs} nor ${entryPointJs} exists.`); |
| 114 | process.exit(1); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | console.log(`Output Build Dir: ${buildDir}`); |
| 119 | console.log(`GN Target: //${gnTarget}`); |
| 120 | console.log(`TS/JS Entry Point: ${entryPoint}`); |
| 121 | |
| 122 | try { |
| 123 | // Ask TypeScript to enumerate the files it knows about. |
| 124 | const types = path.join(cwd, 'front_end/global_typings'); |
Jack Franklin | 15a98cd | 2020-07-23 15:40:32 | [diff] [blame] | 125 | const tscPath = path.join(cwd, 'node_modules', '.bin', 'tsc'); |
Simon Zünd | 6d32fb1 | 2021-01-14 07:58:06 | [diff] [blame] | 126 | const tscOut = await exec(`${tscPath} ${entryPoint} --types ${types}/request_idle_callback.d.ts --types ${ |
Simon Zünd | e4da47f | 2021-05-11 06:15:58 | [diff] [blame] | 127 | types}/global_defs.d.ts --types ${ |
| 128 | types}/intl_display_names.d.ts --noEmit --listFiles --allowJs --target esnext`); |
Paul Lewis | a1448c2 | 2020-07-23 12:22:04 | [diff] [blame] | 129 | |
| 130 | // Filter the list and remap to those that are explicitly in the front_end, excluding the entrypoint itself. |
| 131 | const frontEndFiles = |
| 132 | tscOut.split('\n') |
Paul Lewis | 2f7079d | 2020-07-23 13:30:23 | [diff] [blame] | 133 | .filter(line => line.includes('front_end') && line !== entryPoint && !line.includes('global_typings')) |
Paul Lewis | a1448c2 | 2020-07-23 12:22:04 | [diff] [blame] | 134 | // Ensure we look for the original file, not the .d.ts. |
| 135 | .map(line => line.replace(/\.d\.ts$/, '')) |
Paul Lewis | 2f7079d | 2020-07-23 13:30:23 | [diff] [blame] | 136 | // Ensure that any file that ends in .ts is replaced as the equivalent outputted .js file. |
| 137 | .map(line => line.replace(/\.ts$/, '.js')) |
Paul Lewis | a1448c2 | 2020-07-23 12:22:04 | [diff] [blame] | 138 | // Trim the files so that the path starts with front_end |
| 139 | .map(line => line.replace(/.*?front_end/, 'front_end')) |
| 140 | // Finally remove any files where stripping the .d.ts has resulted in a file with no suffix. |
| 141 | .filter(line => path.extname(line) !== ''); |
| 142 | |
| 143 | // Ask gn/ninja to do the same. |
| 144 | const gnFiles = await buildTargetInfo(buildDir, gnTarget); |
| 145 | |
| 146 | // Then cross reference. |
| 147 | const missingFiles = []; |
| 148 | for (const file of frontEndFiles) { |
| 149 | if (!gnFiles.includes(file)) { |
| 150 | missingFiles.push(file); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | if (missingFiles.length) { |
| 155 | console.error('TypeScript indicates it expects the following files:'); |
| 156 | console.error(missingFiles.map(file => ` - ${file}`).join('\n')); |
| 157 | console.error(`There ${ |
| 158 | missingFiles.length === 1 ? 'is 1 file' : |
| 159 | `are ${missingFiles.length} files`} not listed in the BUILD.gn as dependencies`); |
| 160 | console.error('Have you added all dependencies to the BUILD.gn?'); |
| 161 | process.exit(1); |
| 162 | } else { |
| 163 | console.log('No mismatches found'); |
| 164 | process.exit(0); |
| 165 | } |
| 166 | } catch (e) { |
| 167 | console.error(e); |
| 168 | process.exit(1); |
| 169 | } |
| 170 | })(); |