microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
src/common/exponent/exponentHelper.ts
473lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. |
| 3 | |
| 4 | import * as path from "path"; |
| 5 | import * as Q from "q"; |
| 6 | import * as XDL from "./xdlInterface"; |
| 7 | import stripJsonComments = require("strip-json-comments"); |
| 8 | |
| 9 | import {FileSystem} from "../node/fileSystem"; |
| 10 | import {Package} from "../node/package"; |
| 11 | import {ReactNativeProjectHelper} from "../reactNativeProjectHelper"; |
| 12 | import {CommandVerbosity, CommandExecutor} from "../commandExecutor"; |
| 13 | import {HostPlatform} from "../hostPlatform"; |
| 14 | import {Log} from "../log/log"; |
| 15 | |
| 16 | const VSCODE_EXPONENT_JSON = "vscodeExponent.json"; |
| 17 | const EXPONENT_ENTRYPOINT = "exponentEntrypoint.js"; |
| 18 | const EXPONENT_INDEX = "exponentIndex.js"; |
| 19 | const DEFAULT_IOS_INDEX = "index.ios.js"; |
| 20 | const DEFAULT_ANDROID_INDEX = "index.android.js"; |
| 21 | const EXP_JSON = "exp.json"; |
| 22 | const SECONDS_IN_DAY = 86400; |
| 23 | |
| 24 | enum ReactNativePackageStatus { |
| 25 | FACEBOOK_PACKAGE, |
| 26 | EXPONENT_PACKAGE, |
| 27 | UNKNOWN |
| 28 | } |
| 29 | |
| 30 | export class ExponentHelper { |
| 31 | private rootPath: string; |
| 32 | private fileSystem: FileSystem; |
| 33 | private commandExecutor: CommandExecutor; |
| 34 | |
| 35 | private expSdkVersion: string; |
| 36 | private entrypointFilename: string; |
| 37 | private entrypointComponentName: string; |
| 38 | |
| 39 | private dependencyPackage: ReactNativePackageStatus; |
| 40 | private hasInitialized: boolean; |
| 41 | |
| 42 | public constructor(projectRootPath: string) { |
| 43 | this.rootPath = projectRootPath; |
| 44 | this.hasInitialized = false; |
| 45 | // Constructor is slim by design. This is to add as less computation as possible |
| 46 | // to the initialization of the extension. If a public method is added, make sure |
| 47 | // to call this.lazilyInitialize() at the begining of the code to be sure all variables |
| 48 | // are correctly initialized. |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * File used as an entrypoint for exponent. This file's component should be registered as "main" |
| 53 | * in the AppRegistry and should only render a entrypoint component. |
| 54 | */ |
| 55 | public createIndex(): Q.Promise<void> { |
| 56 | this.lazilyInitialize(); |
| 57 | const pkg = require("../../../package.json"); |
| 58 | const extensionVersionNumber = pkg.version; |
| 59 | const extensionName = pkg.name; |
| 60 | |
| 61 | return this.entrypointComponent() |
| 62 | .then((componentName) => { |
| 63 | const fileContents = |
| 64 | `// This file is automatically generated by ${extensionName}@${extensionVersionNumber} |
| 65 | // Please do not modify it manually. All changes will be lost. |
| 66 | var React = require('react'); |
| 67 | var {Component} = React; |
| 68 | |
| 69 | var ReactNative = require('react-native'); |
| 70 | var {AppRegistry} = ReactNative; |
| 71 | |
| 72 | var EntryPoint = require('./exponentEntrypoint.js'); |
| 73 | var {${componentName}} = EntryPoint; |
| 74 | |
| 75 | class ExponentVSCodeEntryPoint extends Component { |
| 76 | render() { |
| 77 | return ( |
| 78 | <${componentName} /> |
| 79 | ); |
| 80 | } |
| 81 | } |
| 82 | AppRegistry.registerComponent('main', () => ExponentVSCodeEntryPoint);`; |
| 83 | return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_INDEX), fileContents); |
| 84 | }); |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Entrypoint file for exponent. Should copy the file used by the user as an entrypoint and modify it so that it can |
| 89 | * live inside .vscode and can be imported by exponentIndex. |
| 90 | * More specifically it will export the class and update all the relative references. |
| 91 | */ |
| 92 | public createExponentEntrypoint(): Q.Promise<void> { |
| 93 | this.lazilyInitialize(); |
| 94 | const pkg = require("../../../package.json"); |
| 95 | const extensionVersionNumber = pkg.version; |
| 96 | const extensionName = pkg.name; |
| 97 | let entrypointComponentName = ""; |
| 98 | |
| 99 | const fileHeader = |
| 100 | `// This file is automatically generated by ${extensionName}@${extensionVersionNumber} |
| 101 | // Please do not modify it manually. All changes will be lost.\n`; |
| 102 | |
| 103 | return this.entrypointComponent() |
| 104 | .then(component => { |
| 105 | entrypointComponentName = component; |
| 106 | return this.entrypoint(); |
| 107 | }) |
| 108 | .then(filename => { |
| 109 | const pathToEntrypoint = this.pathToFileInWorkspace(filename); |
| 110 | return this.fileSystem.readFile(pathToEntrypoint, "utf-8"); |
| 111 | }) |
| 112 | .then(entrypointContents => { |
| 113 | // Export class |
| 114 | let modifiedContents = fileHeader + entrypointContents; |
| 115 | modifiedContents = modifiedContents.replace(`class ${entrypointComponentName}`, `export class ${entrypointComponentName}`); |
| 116 | |
| 117 | // Prepend ../ to relative imports that refer to something in a parent directory |
| 118 | modifiedContents = modifiedContents.replace(/\.\.\/.*/, (substr) => { return `../${substr}`; }); |
| 119 | |
| 120 | // Replace ./ with ../ in relative imports that refer to something in the same directory |
| 121 | modifiedContents = modifiedContents.replace(/\.\/.*/, (substr) => { return `.${substr}`; }); |
| 122 | |
| 123 | return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_ENTRYPOINT), modifiedContents); |
| 124 | }).catch(() => { |
| 125 | return Q.reject<void>(new Error("Unable to read entrypoint. Make sure the file exists and it's under the root directory.")); |
| 126 | }); |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * Convert react native project to exponent. |
| 131 | * This consists on three steps: |
| 132 | * 1. Change the dependency from facebook's react-native to exponent's fork |
| 133 | * 2. Create exp.json |
| 134 | * 3. Create index and entrypoint for exponent |
| 135 | */ |
| 136 | public configureExponentEnvironment(): Q.Promise<void> { |
| 137 | this.lazilyInitialize(); |
| 138 | Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while..."); |
| 139 | return this.changeReactNativeToExponent() |
| 140 | .then(() => { |
| 141 | Log.logMessage("Dependencies are correct. Making sure you have any necessary configuration file."); |
| 142 | return this.ensureExpJson(); |
| 143 | }).then(() => { |
| 144 | Log.logMessage("Project setup is correct. Generating entrypoint code."); |
| 145 | return this.createExponentEntrypoint() |
| 146 | .then(() => |
| 147 | this.createIndex()); |
| 148 | }); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Change dependencies to point to original react-native repo |
| 153 | */ |
| 154 | public configureReactNativeEnvironment(): Q.Promise<void> { |
| 155 | this.lazilyInitialize(); |
| 156 | Log.logMessage("Checking react native is correctly setup. This may take a while..."); |
| 157 | return this.changeExponentToReactNative(); |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * Returns the current user. If there is none, asks user for username and password and logins to exponent servers. |
| 162 | */ |
| 163 | public loginToExponent(promptForInformation: (message: string, password: boolean) => Q.Promise<string>, showMessage: (message: string) => Q.Promise<string>): Q.Promise<XDL.IUser> { |
| 164 | this.lazilyInitialize(); |
| 165 | return XDL.currentUser() |
| 166 | .then((user) => { |
| 167 | if (!user) { |
| 168 | let username = ""; |
| 169 | return showMessage("You need to login to exponent. Please provide username and password to login. If you don't have an account we will create one for you.") |
| 170 | .then(() => |
| 171 | promptForInformation("Exponent username", false) |
| 172 | ).then((name) => { |
| 173 | username = name; |
| 174 | return promptForInformation("Exponent password", true); |
| 175 | }) |
| 176 | .then((password) => |
| 177 | XDL.login(username, password)); |
| 178 | } |
| 179 | return user; |
| 180 | }) |
| 181 | .catch(error => { |
| 182 | return Q.reject<XDL.IUser>(error); |
| 183 | }); |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Create exp.json file in the workspace root if not present |
| 188 | */ |
| 189 | private ensureExpJson(): Q.Promise<void> { |
| 190 | this.lazilyInitialize(); |
| 191 | let defaultSettings = { |
| 192 | "sdkVersion": "", |
| 193 | "entryPoint": EXPONENT_INDEX, |
| 194 | "slug": "", |
| 195 | "name": "", |
| 196 | }; |
| 197 | return this.readVscodeExponentSettingFile() |
| 198 | .then(exponentJson => { |
| 199 | const expJsonPath = this.pathToFileInWorkspace(EXP_JSON); |
| 200 | if (!this.fileSystem.existsSync(expJsonPath) || exponentJson.overwriteExpJson) { |
| 201 | return this.getPackageName() |
| 202 | .then(name => { |
| 203 | // Name and slug are supposed to be the same, |
| 204 | // but slug only supports alpha numeric and -, |
| 205 | // while name should be human readable |
| 206 | defaultSettings.slug = name.replace(" ", "-"); |
| 207 | defaultSettings.name = name; |
| 208 | return this.exponentSdk(); |
| 209 | }) |
| 210 | .then(exponentVersion => { |
| 211 | if (!exponentVersion) { |
| 212 | return XDL.supportedVersions() |
| 213 | .then((versions) => { |
| 214 | return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`)); |
| 215 | }); |
| 216 | } |
| 217 | defaultSettings.sdkVersion = exponentVersion; |
| 218 | return this.fileSystem.writeFile(expJsonPath, JSON.stringify(defaultSettings, null, 4)); |
| 219 | }); |
| 220 | } |
| 221 | }); |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Changes npm dependency from react native to exponent's fork |
| 226 | */ |
| 227 | private changeReactNativeToExponent(): Q.Promise<void> { |
| 228 | Log.logString("Checking if react native is from exponent."); |
| 229 | return this.usingReactNativeExponent(true) |
| 230 | .then(usingExponent => { |
| 231 | Log.logString(".\n"); |
| 232 | if (usingExponent) { |
| 233 | return Q.resolve<void>(void 0); |
| 234 | } |
| 235 | Log.logString("Getting appropriate Exponent SDK Version to install."); |
| 236 | return this.exponentSdk(true) |
| 237 | .then(sdkVersion => { |
| 238 | Log.logString(".\n"); |
| 239 | if (!sdkVersion) { |
| 240 | return XDL.supportedVersions() |
| 241 | .then((versions) => { |
| 242 | return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`)); |
| 243 | }); |
| 244 | } |
| 245 | const exponentFork = `github:exponentjs/react-native#sdk-${sdkVersion}`; |
| 246 | Log.logString("Uninstalling current react native package."); |
| 247 | return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS })) |
| 248 | .then(() => { |
| 249 | Log.logString("Installing exponent react native package."); |
| 250 | return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", exponentFork, "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS }); |
| 251 | }); |
| 252 | }); |
| 253 | }) |
| 254 | .then(() => { |
| 255 | this.dependencyPackage = ReactNativePackageStatus.EXPONENT_PACKAGE; |
| 256 | }); |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Changes npm dependency from exponent's fork to react native |
| 261 | */ |
| 262 | private changeExponentToReactNative(): Q.Promise<void> { |
| 263 | Log.logString("Checking if the correct react native is installed."); |
| 264 | return this.usingReactNativeExponent() |
| 265 | .then(usingExponent => { |
| 266 | Log.logString(".\n"); |
| 267 | if (!usingExponent) { |
| 268 | return Q.resolve<void>(void 0); |
| 269 | } |
| 270 | Log.logString("Uninstalling current react native package."); |
| 271 | return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS })) |
| 272 | .then(() => { |
| 273 | Log.logString("Installing correct react native package."); |
| 274 | return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", "react-native", "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS }); |
| 275 | }); |
| 276 | }) |
| 277 | .then(() => { |
| 278 | this.dependencyPackage = ReactNativePackageStatus.FACEBOOK_PACKAGE; |
| 279 | }); |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * Reads VSCODE_EXPONENT Settings file. If it doesn't exists it creates one by |
| 284 | * guessing which entrypoint and filename to use. |
| 285 | */ |
| 286 | private readVscodeExponentSettingFile(): Q.Promise<any> { |
| 287 | // Only create a new one if there is not one already |
| 288 | return this.fileSystem.exists(this.dotvscodePath(VSCODE_EXPONENT_JSON)) |
| 289 | .then((vscodeExponentExists: boolean) => { |
| 290 | if (vscodeExponentExists) { |
| 291 | return this.fileSystem.readFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), "utf-8") |
| 292 | .then(function (jsonContents: string): Q.Promise<any> { |
| 293 | return JSON.parse(stripJsonComments(jsonContents)); |
| 294 | }); |
| 295 | } else { |
| 296 | let defaultSettings = { |
| 297 | "entryPointFilename": "", |
| 298 | "entryPointComponent": "", |
| 299 | "overwriteExpJson": false, |
| 300 | }; |
| 301 | return this.getPackageName() |
| 302 | .then(packageName => { |
| 303 | // By default react-native uses the package name for the entry component. This is our safest guess. |
| 304 | defaultSettings.entryPointComponent = packageName; |
| 305 | this.entrypointComponentName = defaultSettings.entryPointComponent; |
| 306 | return this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)); |
| 307 | }) |
| 308 | .then((indexIosExists: boolean) => { |
| 309 | // If there is an ios entrypoint we want to use that, if not let's go with android |
| 310 | defaultSettings.entryPointFilename = indexIosExists ? DEFAULT_IOS_INDEX : DEFAULT_ANDROID_INDEX; |
| 311 | this.entrypointFilename = defaultSettings.entryPointFilename; |
| 312 | return this.fileSystem.writeFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), JSON.stringify(defaultSettings, null, 4)); |
| 313 | }) |
| 314 | .then(() => { |
| 315 | return defaultSettings; |
| 316 | }); |
| 317 | } |
| 318 | }); |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * Exponent sdk version that maps to the current react-native version |
| 323 | * If react native version is not supported it returns null. |
| 324 | */ |
| 325 | private exponentSdk(showProgress: boolean = false): Q.Promise<string> { |
| 326 | if (showProgress) Log.logString("..."); |
| 327 | if (this.expSdkVersion) { |
| 328 | return Q(this.expSdkVersion); |
| 329 | } |
| 330 | return this.readFromExpJson<string>("sdkVersion") |
| 331 | .then((sdkVersion) => { |
| 332 | if (showProgress) Log.logString("."); |
| 333 | if (sdkVersion) { |
| 334 | this.expSdkVersion = sdkVersion; |
| 335 | return this.expSdkVersion; |
| 336 | } |
| 337 | let reactNativeProjectHelper = new ReactNativeProjectHelper(this.rootPath); |
| 338 | return reactNativeProjectHelper.getReactNativeVersion() |
| 339 | .then(version => { |
| 340 | if (showProgress) Log.logString("."); |
| 341 | return XDL.mapVersion(version) |
| 342 | .then(exponentVersion => { |
| 343 | this.expSdkVersion = exponentVersion; |
| 344 | return this.expSdkVersion; |
| 345 | }); |
| 346 | }); |
| 347 | }); |
| 348 | } |
| 349 | |
| 350 | /** |
| 351 | * Returns the specified setting from exp.json if it exists |
| 352 | */ |
| 353 | private readFromExpJson<T>(setting: string): Q.Promise<T> { |
| 354 | const expJsonPath = this.pathToFileInWorkspace(EXP_JSON); |
| 355 | return this.fileSystem.exists(expJsonPath) |
| 356 | .then((exists: boolean) => { |
| 357 | if (!exists) { |
| 358 | return null; |
| 359 | } |
| 360 | return this.fileSystem.readFile(expJsonPath, "utf-8") |
| 361 | .then(function (jsonContents: string): Q.Promise<T> { |
| 362 | return JSON.parse(stripJsonComments(jsonContents))[setting]; |
| 363 | }); |
| 364 | }); |
| 365 | } |
| 366 | |
| 367 | /** |
| 368 | * Looks at the _from attribute in the package json of the react-native dependency |
| 369 | * to figure out if it's using exponent. |
| 370 | */ |
| 371 | private usingReactNativeExponent(showProgress: boolean = false): Q.Promise<boolean> { |
| 372 | if (showProgress) Log.logString("..."); |
| 373 | if (this.dependencyPackage !== ReactNativePackageStatus.UNKNOWN) { |
| 374 | return Q(this.dependencyPackage === ReactNativePackageStatus.EXPONENT_PACKAGE); |
| 375 | } |
| 376 | // Look for the package.json of the dependecy |
| 377 | const pathToReactNativePackageJson = path.resolve(this.rootPath, "node_modules", "react-native", "package.json"); |
| 378 | return this.fileSystem.readFile(pathToReactNativePackageJson, "utf-8") |
| 379 | .then(jsonContents => { |
| 380 | const packageJson = JSON.parse(jsonContents); |
| 381 | const isExp = /\bexponentjs\/react-native\b/.test(packageJson._from); |
| 382 | this.dependencyPackage = isExp ? ReactNativePackageStatus.EXPONENT_PACKAGE : ReactNativePackageStatus.FACEBOOK_PACKAGE; |
| 383 | if (showProgress) Log.logString("."); |
| 384 | return isExp; |
| 385 | }).catch(() => { |
| 386 | if (showProgress) Log.logString("."); |
| 387 | // Not in a react-native project |
| 388 | return false; |
| 389 | }); |
| 390 | } |
| 391 | |
| 392 | /** |
| 393 | * Name of the file (we assume it lives in the workspace root) that should be used as entrypoint. |
| 394 | * e.g. index.ios.js |
| 395 | */ |
| 396 | private entrypoint(): Q.Promise<string> { |
| 397 | if (this.entrypointFilename) { |
| 398 | return Q(this.entrypointFilename); |
| 399 | } |
| 400 | return this.readVscodeExponentSettingFile() |
| 401 | .then((settingsJson) => { |
| 402 | // Let's load both to memory to make sure we are not reading from memory next time we query for this. |
| 403 | this.entrypointFilename = settingsJson.entryPointFilename; |
| 404 | this.entrypointComponentName = settingsJson.entryPointComponent; |
| 405 | return this.entrypointFilename; |
| 406 | }); |
| 407 | } |
| 408 | |
| 409 | /** |
| 410 | * Name of the component used as an entrypoint for the app. |
| 411 | */ |
| 412 | private entrypointComponent(): Q.Promise<string> { |
| 413 | if (this.entrypointComponentName) { |
| 414 | return Q(this.entrypointComponentName); |
| 415 | } |
| 416 | return this.readVscodeExponentSettingFile() |
| 417 | .then((settingsJson) => { |
| 418 | // Let's load both to memory to make sure we are not reading from memory next time we query for this. |
| 419 | this.entrypointComponentName = settingsJson.entryPointComponent; |
| 420 | this.entrypointFilename = settingsJson.entrypointFilename; |
| 421 | return this.entrypointComponentName; |
| 422 | }); |
| 423 | } |
| 424 | |
| 425 | /** |
| 426 | * Path to the a given file inside the .vscode directory |
| 427 | */ |
| 428 | private dotvscodePath(filename: string): string { |
| 429 | return path.join(this.rootPath, ".vscode", filename); |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Path to the a given file from the workspace root |
| 434 | */ |
| 435 | private pathToFileInWorkspace(filename: string): string { |
| 436 | return path.join(this.rootPath, filename); |
| 437 | } |
| 438 | |
| 439 | /** |
| 440 | * Name specified on user's package.json |
| 441 | */ |
| 442 | private getPackageName(): Q.Promise<string> { |
| 443 | return new Package(this.rootPath, { fileSystem: this.fileSystem }).name(); |
| 444 | } |
| 445 | |
| 446 | /** |
| 447 | * Works as a constructor but only initiliazes when it's actually needed. |
| 448 | */ |
| 449 | private lazilyInitialize(): void { |
| 450 | if (!this.hasInitialized) { |
| 451 | this.hasInitialized = true; |
| 452 | this.fileSystem = new FileSystem(); |
| 453 | this.commandExecutor = new CommandExecutor(this.rootPath); |
| 454 | this.dependencyPackage = ReactNativePackageStatus.UNKNOWN; |
| 455 | |
| 456 | XDL.configReactNativeVersionWargnings(); |
| 457 | XDL.attachLoggerStream(this.rootPath, { |
| 458 | stream: { |
| 459 | write: (chunk: any) => { |
| 460 | if (chunk.level <= 30) { |
| 461 | Log.logString(chunk.msg); |
| 462 | } else if (chunk.level === 40) { |
| 463 | Log.logWarning(chunk.msg); |
| 464 | } else { |
| 465 | Log.logError(chunk.msg); |
| 466 | } |
| 467 | }, |
| 468 | }, |
| 469 | type: "raw", |
| 470 | }); |
| 471 | } |
| 472 | } |
| 473 | } |
| 474 | |