1'use strict'; 2/* eslint-env node */ 3 4// This script is just a wrapper around expotools that ensures node modules are installed 5// and TypeScript files are compiled. To make it work even when node_modules are empty, 6// we shouldn't eagerly require any dependency - we have to run yarn first. 7 8const child_process = require('child_process'); 9const fs = require('fs'); 10const path = require('path'); 11 12const ROOT_PATH = path.dirname(__dirname); 13const BUILD_PATH = path.join(ROOT_PATH, 'build'); 14const STATE_PATH = path.join(ROOT_PATH, 'cache', '.state.json'); 15 16function createLogModifier(modifier) { 17 return (text) => { 18 try { 19 return modifier(require('chalk'))(text); 20 } catch (e) { 21 return text; 22 } 23 }; 24} 25/** 26 * Importing chalk directly may lead to errors 27 * if it's not yet available on the machine. 28 * 29 * Intermediary log modifiers catch the error 30 * and return unmodified string passed into the logger. 31 * 32 * See https://github.com/expo/expo/issues/9547 33 */ 34const LogModifiers = { 35 error: createLogModifier((chalk) => chalk.red), 36 name: createLogModifier((chalk) => chalk.cyan), 37 command: createLogModifier((chalk) => chalk.cyan.italic), 38}; 39 40maybeRebuildAndRun().catch((error) => { 41 console.error(LogModifiers.error(error.stack)); 42}); 43 44async function maybeRebuildAndRun() { 45 const state = readState(); 46 const dependenciesChecksum = await calculateDependenciesChecksumAsync(); 47 const sourceChecksum = await calculateSourceChecksumAsync(); 48 49 // If `yarn.lock` checksum changed, reinstall expotools dependencies. 50 if (!state.dependenciesChecksum || state.dependenciesChecksum !== dependenciesChecksum) { 51 console.log(' Yarning...'); 52 await spawnAsync('yarn', ['install']); 53 } 54 55 // If checksum of source files changed, rebuild TypeScript files. 56 if (!state.sourceChecksum || state.sourceChecksum !== sourceChecksum || !buildFolderExists()) { 57 console.log(` Rebuilding ${LogModifiers.name('expotools')}`); 58 59 try { 60 // Compile TypeScript files into build folder. 61 await spawnAsync('yarn', ['run', 'build']); 62 state.schema = await getCommandsSchemaAsync(); 63 } catch (error) { 64 console.error(LogModifiers.error(` Rebuilding failed: ${error.stack}`)); 65 process.exit(1); 66 } 67 console.log(` ✨ Successfully built ${LogModifiers.name('expotools')}\n`); 68 } 69 70 state.sourceChecksum = sourceChecksum || (await calculateSourceChecksumAsync()); 71 state.dependenciesChecksum = dependenciesChecksum || (await calculateDependenciesChecksumAsync()); 72 73 saveState(state); 74 run(state.schema); 75} 76 77function buildFolderExists() { 78 try { 79 fs.accessSync(BUILD_PATH, fs.constants.R_OK); 80 return true; 81 } catch (e) { 82 return false; 83 } 84} 85 86async function calculateChecksumAsync(options) { 87 if (canRequire('folder-hash')) { 88 const { hashElement } = require('folder-hash'); 89 const { hash } = await hashElement(ROOT_PATH, options); 90 return hash; 91 } 92 return null; 93} 94 95async function calculateDependenciesChecksumAsync() { 96 return calculateChecksumAsync({ 97 folders: { 98 exclude: ['*'], 99 }, 100 files: { 101 include: ['yarn.lock', 'package.json'], 102 }, 103 }); 104} 105 106async function calculateSourceChecksumAsync() { 107 return calculateChecksumAsync({ 108 folders: { 109 exclude: ['build', 'cache', 'node_modules'], 110 }, 111 files: { 112 include: [ 113 // source files 114 '**.ts', 115 // src/versioning files 116 '**.json', 117 'expotools.js', 118 // swc build files 119 'taskfile.js', 120 'taskfile-swc.js', 121 // type checking 122 'tsconfig.json', 123 ], 124 }, 125 }); 126} 127 128function loadCommand(program, commandFile) { 129 const commandModule = require(commandFile); 130 131 if (typeof commandModule.default !== 'function') { 132 console.error( 133 `Command file "${commandFile}" is not valid. Make sure to export command function as a default.` 134 ); 135 return; 136 } 137 commandModule.default(program); 138} 139 140async function loadAllCommandsAsync(callback) { 141 const program = require('@expo/commander'); 142 const glob = require('glob-promise'); 143 144 const commandFiles = await glob('build/commands/*.js', { 145 cwd: ROOT_PATH, 146 absolute: true, 147 }); 148 149 for (const commandFile of commandFiles) { 150 loadCommand(program, commandFile); 151 152 if (callback) { 153 callback(commandFile, program); 154 } 155 } 156} 157 158async function getCommandsSchemaAsync() { 159 const schema = {}; 160 161 await loadAllCommandsAsync((commandFile, program) => { 162 for (const command of program.commands) { 163 const names = [command._name]; 164 165 if (command._aliases) { 166 names.push(...command._aliases); 167 } 168 for (const name of names) { 169 if (!schema[name]) { 170 schema[name] = path.relative(BUILD_PATH, commandFile); 171 } 172 } 173 } 174 }); 175 return schema; 176} 177 178function readState() { 179 if (canRequire(STATE_PATH)) { 180 return require(STATE_PATH); 181 } 182 return { 183 sourceChecksum: null, 184 dependenciesChecksum: null, 185 schema: null, 186 }; 187} 188 189function saveState(state) { 190 fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true }); 191 fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2)); 192} 193 194function spawnAsync(command, args, options) { 195 return new Promise((resolve, reject) => { 196 const child = child_process.spawn( 197 command, 198 args, 199 options || { 200 stdio: ['pipe', 'ignore', 'pipe'], 201 ignoreStdio: true, 202 cwd: ROOT_PATH, 203 } 204 ); 205 206 child.on('exit', (code) => { 207 child.removeAllListeners(); 208 resolve({ code }); 209 }); 210 child.on('error', (error) => { 211 child.removeAllListeners(); 212 reject(error); 213 }); 214 }); 215} 216 217function canRequire(packageName) { 218 try { 219 require.resolve(packageName); 220 return true; 221 } catch { 222 return false; 223 } 224} 225 226async function run(schema) { 227 const semver = require('semver'); 228 const program = require('@expo/commander'); 229 const nodeVersion = process.versions.node.split('-')[0]; // explode and truncate tag from version 230 231 // Validate that used Node version is supported 232 if (semver.satisfies(nodeVersion, '<8.9.0')) { 233 console.log( 234 LogModifiers.error( 235 `Node version ${LogModifiers.name( 236 nodeVersion 237 )} is not supported. Please use Node.js ${LogModifiers.name('8.9.0')} or higher.` 238 ) 239 ); 240 process.exit(1); 241 } 242 243 try { 244 const subCommandName = process.argv[2]; 245 246 if (subCommandName && !subCommandName.startsWith('-')) { 247 if (!schema[subCommandName]) { 248 console.log( 249 LogModifiers.error( 250 `${LogModifiers.command(subCommandName)} is not an expotools command.` 251 ), 252 LogModifiers.error( 253 `Run ${LogModifiers.command('et --help')} to see a list of available commands.\n` 254 ) 255 ); 256 process.exit(1); 257 return; 258 } 259 260 const subCommand = 261 subCommandName && 262 program.commands.find(({ _name, _aliases }) => { 263 return _name === subCommandName || (_aliases && _aliases.includes(subCommandName)); 264 }); 265 266 if (!subCommand) { 267 // If the command is known and defined in schema, load just this one command and run it. 268 const commandFilePath = path.join(BUILD_PATH, schema[subCommandName]); 269 loadCommand(program, commandFilePath); 270 } 271 272 // Pass args to commander and run the command. 273 program.parse(process.argv); 274 } else { 275 await loadAllCommandsAsync(); 276 program.help(); 277 } 278 } catch (e) { 279 console.error(LogModifiers.error(e)); 280 throw e; 281 } 282} 283