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', 'tsc']); 62 state.schema = await getCommandsSchemaAsync(); 63 } catch (error) { 64 console.error(LogModifiers.error(` Rebuilding failed: ${error.stack}`)); 65 process.exit(1); 66 return; 67 } 68 console.log(` ✨ Successfully built ${LogModifiers.name('expotools')}\n`); 69 } 70 71 state.sourceChecksum = sourceChecksum || (await calculateSourceChecksumAsync()); 72 state.dependenciesChecksum = dependenciesChecksum || (await calculateDependenciesChecksumAsync()); 73 74 saveState(state); 75 run(state.schema); 76} 77 78function buildFolderExists() { 79 try { 80 fs.accessSync(BUILD_PATH, fs.constants.R_OK); 81 return true; 82 } catch (e) { 83 return false; 84 } 85} 86 87async function calculateChecksumAsync(options) { 88 if (canRequire('folder-hash')) { 89 const { hashElement } = require('folder-hash'); 90 const { hash } = await hashElement(ROOT_PATH, options); 91 return hash; 92 } 93 return null; 94} 95 96async function calculateDependenciesChecksumAsync() { 97 return calculateChecksumAsync({ 98 folders: { 99 exclude: ['*'], 100 }, 101 files: { 102 include: ['yarn.lock', 'package.json'], 103 }, 104 }); 105} 106 107async function calculateSourceChecksumAsync() { 108 return calculateChecksumAsync({ 109 folders: { 110 exclude: ['build', 'cache', 'node_modules'], 111 }, 112 files: { 113 include: ['*.ts', 'expotools.js', 'tsconfig.json'], 114 }, 115 }); 116} 117 118function loadCommand(program, commandFile) { 119 const commandModule = require(commandFile); 120 121 if (typeof commandModule.default !== 'function') { 122 console.error( 123 `Command file "${commandFile}" is not valid. Make sure to export command function as a default.` 124 ); 125 return; 126 } 127 commandModule.default(program); 128} 129 130async function loadAllCommandsAsync(callback) { 131 const program = require('@expo/commander'); 132 const glob = require('glob-promise'); 133 134 const commandFiles = await glob('build/commands/*.js', { 135 cwd: ROOT_PATH, 136 absolute: true, 137 }); 138 139 for (const commandFile of commandFiles) { 140 loadCommand(program, commandFile); 141 142 if (callback) { 143 callback(commandFile, program); 144 } 145 } 146} 147 148async function getCommandsSchemaAsync() { 149 const schema = {}; 150 151 await loadAllCommandsAsync((commandFile, program) => { 152 for (const command of program.commands) { 153 const names = [command._name]; 154 155 if (command._aliases) { 156 names.push(...command._aliases); 157 } 158 for (const name of names) { 159 if (!schema[name]) { 160 schema[name] = path.relative(BUILD_PATH, commandFile); 161 } 162 } 163 } 164 }); 165 return schema; 166} 167 168function readState() { 169 if (canRequire(STATE_PATH)) { 170 return require(STATE_PATH); 171 } 172 return { 173 sourceChecksum: null, 174 dependenciesChecksum: null, 175 schema: null, 176 }; 177} 178 179function saveState(state) { 180 fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2)); 181} 182 183function spawnAsync(command, args, options) { 184 return new Promise((resolve, reject) => { 185 const child = child_process.spawn( 186 command, 187 args, 188 options || { 189 stdio: ['pipe', 'ignore', 'pipe'], 190 ignoreStdio: true, 191 cwd: ROOT_PATH, 192 } 193 ); 194 195 child.on('exit', (code) => { 196 child.removeAllListeners(); 197 resolve({ code }); 198 }); 199 child.on('error', (error) => { 200 child.removeAllListeners(); 201 reject(error); 202 }); 203 }); 204} 205 206function canRequire(packageName) { 207 try { 208 require.resolve(packageName); 209 return true; 210 } catch (error) { 211 return false; 212 } 213} 214 215async function run(schema) { 216 const semver = require('semver'); 217 const program = require('@expo/commander'); 218 const nodeVersion = process.versions.node.split('-')[0]; // explode and truncate tag from version 219 220 // Validate that used Node version is supported 221 if (semver.satisfies(nodeVersion, '<8.9.0')) { 222 console.log( 223 LogModifiers.error( 224 `Node version ${LogModifiers.name( 225 nodeVersion 226 )} is not supported. Please use Node.js ${LogModifiers.name('8.9.0')} or higher.` 227 ) 228 ); 229 process.exit(1); 230 } 231 232 try { 233 const subCommandName = process.argv[2]; 234 235 if (subCommandName && !subCommandName.startsWith('-')) { 236 if (!schema[subCommandName]) { 237 console.log( 238 LogModifiers.error( 239 `${LogModifiers.command(subCommandName)} is not an expotools command.` 240 ), 241 LogModifiers.error( 242 `Run ${LogModifiers.command('et --help')} to see a list of available commands.\n` 243 ) 244 ); 245 process.exit(1); 246 return; 247 } 248 249 const subCommand = 250 subCommandName && 251 program.commands.find(({ _name, _aliases }) => { 252 return _name === subCommandName || (_aliases && _aliases.includes(subCommandName)); 253 }); 254 255 if (!subCommand) { 256 // If the command is known and defined in schema, load just this one command and run it. 257 const commandFilePath = path.join(BUILD_PATH, schema[subCommandName]); 258 loadCommand(program, commandFilePath); 259 } 260 261 // Pass args to commander and run the command. 262 program.parse(process.argv); 263 } else { 264 await loadAllCommandsAsync(); 265 program.help(); 266 } 267 } catch (e) { 268 console.error(LogModifiers.error(e)); 269 throw e; 270 } 271} 272