From 8e7a8fa8d818ec2fd0c51e35d7a16d3bd2f0223a Mon Sep 17 00:00:00 2001 From: Artem Egorov Date: Mon, 17 Jul 2017 17:07:48 +0300 Subject: [PATCH 1/4] Code refactoring for better Expo support --- src/common/commandExecutor.ts | 14 +- src/common/exponent/exponentHelper.d.ts | 14 + src/common/exponent/exponentHelper.ts | 395 +++++------------------- src/common/exponent/xdlInterface.ts | 32 +- src/common/packager.ts | 52 ++-- src/extension/commandPaletteHandler.ts | 5 +- src/extension/extensionServer.ts | 5 +- src/extension/rn-extension.ts | 2 +- 8 files changed, 154 insertions(+), 365 deletions(-) create mode 100644 src/common/exponent/exponentHelper.d.ts diff --git a/src/common/commandExecutor.ts b/src/common/commandExecutor.ts index 9a19cd0c1..335e2675e 100644 --- a/src/common/commandExecutor.ts +++ b/src/common/commandExecutor.ts @@ -32,7 +32,9 @@ export enum CommandStatus { } export class CommandExecutor { - private static ReactNativeCommand = "react-native"; + + private static ReactNativeGlobal = "react-native"; + private static ReactNativeCLI = "node_modules/react-native/local-cli/cli.js"; private static ReactNativeVersionCommand = "-v"; private currentWorkingDirectory: string; private childProcess = new Node.ChildProcess(); @@ -76,7 +78,7 @@ export class CommandExecutor { */ public getReactNativeVersion(): Q.Promise { let deferred = Q.defer(); - const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeCommand); + const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeGlobal); let output = ""; const result = this.childProcess.spawn(reactCommand, @@ -120,8 +122,8 @@ export class CommandExecutor { * Executes a react native command and waits for its completion. */ public spawnReactCommand(command: string, args?: string[], options: Options = {}): ISpawnResult { - const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeCommand); - return this.spawnChildProcess(reactCommand, this.combineArguments(command, args), options); + const reactCommand = CommandExecutor.ReactNativeCLI; + return this.spawnChildProcess("node", [reactCommand, command, ...args], options); } /** @@ -209,8 +211,4 @@ export class CommandExecutor { private generateRejectionForCommand(command: string, reason: any): Q.Promise { return Q.reject(ErrorHelper.getNestedError(reason, InternalErrorCode.CommandFailed, command)); } - - private combineArguments(firstArgument: string, otherArguments: string[] = []) { - return [firstArgument].concat(otherArguments); - } } diff --git a/src/common/exponent/exponentHelper.d.ts b/src/common/exponent/exponentHelper.d.ts new file mode 100644 index 000000000..6b5430390 --- /dev/null +++ b/src/common/exponent/exponentHelper.d.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +declare class ExpConfig { + name: string; + slug: string; + sdkVersion: string; + version?: string; + packagerOpts?: ExpConfigPackager; +} + +declare class ExpConfigPackager { + assetExts? : string[]; +} \ No newline at end of file diff --git a/src/common/exponent/exponentHelper.ts b/src/common/exponent/exponentHelper.ts index efeea0981..e53cddb9a 100644 --- a/src/common/exponent/exponentHelper.ts +++ b/src/common/exponent/exponentHelper.ts @@ -1,47 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +/// + import * as path from "path"; import * as Q from "q"; -import * as XDL from "./xdlInterface"; -import stripJsonComments = require("strip-json-comments"); +import stripJSONComments = require("strip-json-comments"); -import {FileSystem} from "../node/fileSystem"; import {Package} from "../node/package"; -import {ReactNativeProjectHelper} from "../reactNativeProjectHelper"; -import {CommandVerbosity, CommandExecutor} from "../commandExecutor"; -import {HostPlatform} from "../hostPlatform"; -import {Log} from "../log/log"; +import * as XDL from "./xdlInterface"; +import { FileSystem } from "../node/fileSystem"; +import { Log } from "../log/log"; -const VSCODE_EXPONENT_JSON = "vscodeExponent.json"; -const EXPONENT_INDEX = "exponentIndex.js"; -const DEFAULT_EXPONENT_INDEX = "main.js"; -const DEFAULT_IOS_INDEX = "index.ios.js"; -const DEFAULT_ANDROID_INDEX = "index.android.js"; +const APP_JSON = "app.json"; const EXP_JSON = "exp.json"; -const SECONDS_IN_DAY = 86400; - -enum ReactNativePackageStatus { - FACEBOOK_PACKAGE, - EXPONENT_PACKAGE, - UNKNOWN -} export class ExponentHelper { private projectRootPath: string; - private workspaceRootPath: string; - private fileSystem: FileSystem; - private commandExecutor: CommandExecutor; - - private expSdkVersion: string; - private entrypointFilename: string; - private entrypointComponentName: string; - - private dependencyPackage: ReactNativePackageStatus; + private fs: FileSystem; private hasInitialized: boolean; - public constructor(workspaceRootPath: string, projectRootPath: string) { - this.workspaceRootPath = workspaceRootPath; + public constructor(projectRootPath: string) { this.projectRootPath = projectRootPath; this.hasInitialized = false; // Constructor is slim by design. This is to add as less computation as possible @@ -50,33 +29,15 @@ export class ExponentHelper { // are correctly initialized. } - /** - * Convert react native project to exponent. - * This consists on three steps: - * 1. Change the dependency from facebook's react-native to exponent's fork - * 2. Create exp.json - * 3. Create index and entrypoint for exponent - */ public configureExponentEnvironment(): Q.Promise { this.lazilyInitialize(); Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while..."); - return this.changeReactNativeToExponent() - .then(() => { - Log.logMessage("Dependencies are correct. Making sure you have any necessary configuration file."); - return this.ensureExpJson(); - }).then(() => { - Log.logMessage("Project setup is correct. Generating entrypoint code."); - return this.createIndex(); - }); - } + return this.isExpoApp(true) + .then(isExpo => { + Log.logString(".\n"); - /** - * Change dependencies to point to original react-native repo - */ - public configureReactNativeEnvironment(): Q.Promise { - this.lazilyInitialize(); - Log.logMessage("Checking react native is correctly setup. This may take a while..."); - return this.changeExponentToReactNative(); + return this.patchAppJson(); + }); } /** @@ -105,291 +66,80 @@ export class ExponentHelper { }); } - public getExponentPackagerOptions(): Q.Promise { - this.lazilyInitialize(); - return this.readFromExpJson("packagerOpts"); - } - - /** - * File used as an entrypoint for exponent. This file's component should be registered as "main" - * in the AppRegistry and should only render a entrypoint component. - */ - private createIndex(): Q.Promise { + public getExpPackagerOptions(): Q.Promise { this.lazilyInitialize(); - const pkg = require("../../../package.json"); - const extensionVersionNumber = pkg.version; - const extensionName = pkg.name; - - return Q.all([this.entrypointComponent(), this.entrypoint()]) - .spread((componentName: string, entryPointFile: string) => { - const fileContents = - `// This file is automatically generated by ${extensionName}@${extensionVersionNumber} -// Please do not modify it manually. All changes will be lost. -var React = require('${this.projectRootPath}/node_modules/react'); -var {Component} = React; - -var ReactNative = require('${this.projectRootPath}/node_modules/react-native'); -var {AppRegistry} = ReactNative; - -var entryPoint = require('${this.projectRootPath}/${entryPointFile}'); - -AppRegistry.registerRunnable('main', function(appParameters) { - AppRegistry.runApplication('${componentName}', appParameters); -});`; - return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_INDEX), fileContents); - }); + return this.getFromExpConfig("packagerOpts") + .then(opts => opts || {}); } - /** - * Create exp.json file in the workspace root if not present - */ - private ensureExpJson(): Q.Promise { - this.lazilyInitialize(); - let defaultSettings = { - "sdkVersion": "", - "entryPoint": this.dotvscodePath(EXPONENT_INDEX), - "slug": "", - "name": "", - }; - return this.readVscodeExponentSettingFile() - .then(exponentJson => { - const expJsonPath = this.pathToFileInWorkspace(EXP_JSON); - if (!this.fileSystem.existsSync(expJsonPath) || exponentJson.overwriteExpJson) { + private patchAppJson(): Q.Promise { + return this.readAppJson() + .then((config: ExpConfig) => { + if (!config.name || !config.slug) { return this.getPackageName() .then(name => { - // Name and slug are supposed to be the same, - // but slug only supports alpha numeric and -, - // while name should be human readable - defaultSettings.slug = name.replace(" ", "-"); - defaultSettings.name = name; - return this.exponentSdk(); - }) - .then(exponentVersion => { - if (!exponentVersion) { - return XDL.supportedVersions() - .then((versions) => { - return Q.reject(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`)); - }); - } - defaultSettings.sdkVersion = exponentVersion; - return this.fileSystem.writeFile(expJsonPath, JSON.stringify(defaultSettings, null, 4)); + config.slug = config.slug || name.replace(" ", "-"); + config.name = config.name || name; + return config; }); } - }); - } - /** - * Changes npm dependency from react native to exponent's fork - */ - private changeReactNativeToExponent(): Q.Promise { - Log.logString("Checking if react native is from exponent."); - return this.usingReactNativeExponent(true) - .then(usingExponent => { - Log.logString(".\n"); - if (usingExponent) { - return Q.resolve(void 0); - } - Log.logString("Getting appropriate Exponent SDK Version to install."); - return this.exponentSdk(true) - .then(sdkVersion => { - Log.logString(".\n"); - if (!sdkVersion) { - return XDL.supportedVersions() - .then((versions) => { - return Q.reject(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`)); - }); - } - const exponentFork = `github:exponentjs/react-native#sdk-${sdkVersion}`; - Log.logString("Uninstalling current react native package."); - return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS })) - .then(() => { - Log.logString("Installing exponent react native package."); - return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", exponentFork, "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS }); - }); - }); + return Q.resolve(null); }) - .then(() => { - this.dependencyPackage = ReactNativePackageStatus.EXPONENT_PACKAGE; - }); - } - - /** - * Changes npm dependency from exponent's fork to react native - */ - private changeExponentToReactNative(): Q.Promise { - Log.logString("Checking if the correct react native is installed."); - return this.usingReactNativeExponent() - .then(usingExponent => { - Log.logString(".\n"); - if (!usingExponent) { - return Q.resolve(void 0); + .then((config: ExpConfig) => { + if (config) { + return this.writeAppJson(config); } - Log.logString("Uninstalling current react native package."); - return Q(this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["uninstall", "react-native", "--verbose"], { verbosity: CommandVerbosity.PROGRESS })) - .then(() => { - Log.logString("Installing correct react native package."); - return this.commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), ["install", "react-native", "--cache-min", SECONDS_IN_DAY.toString(10), "--verbose"], { verbosity: CommandVerbosity.PROGRESS }); - }); - }) - .then(() => { - this.dependencyPackage = ReactNativePackageStatus.FACEBOOK_PACKAGE; }); - } + }; /** - * Reads VSCODE_EXPONENT Settings file. If it doesn't exists it creates one by - * guessing which entrypoint and filename to use. + * Name specified on user's package.json */ - private readVscodeExponentSettingFile(): Q.Promise { - // Only create a new one if there is not one already - return this.fileSystem.exists(this.dotvscodePath(VSCODE_EXPONENT_JSON)) - .then((vscodeExponentExists: boolean) => { - if (vscodeExponentExists) { - return this.fileSystem.readFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), "utf-8") - .then(function (jsonContents: string): Q.Promise { - return JSON.parse(stripJsonComments(jsonContents)); - }); - } else { - let defaultSettings = { - "entryPointFilename": "", - "entryPointComponent": "", - "overwriteExpJson": false, - }; - return this.getPackageName() - .then(packageName => { - // By default react-native uses the package name for the entry component. This is our safest guess. - defaultSettings.entryPointComponent = packageName; - this.entrypointComponentName = defaultSettings.entryPointComponent; - return Q.all([ - this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)), - this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)), - ]); - }) - .spread((indexIosExists: boolean, mainExists: boolean) => { - // If there is an ios entrypoint we want to use that, if not let's go with android - defaultSettings.entryPointFilename = - mainExists ? DEFAULT_EXPONENT_INDEX - : indexIosExists ? DEFAULT_IOS_INDEX - : DEFAULT_ANDROID_INDEX; - this.entrypointFilename = defaultSettings.entryPointFilename; - return this.fileSystem.writeFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), JSON.stringify(defaultSettings, null, 4)); - }) - .then(() => { - return defaultSettings; - }); - } - }); + private getPackageName(): Q.Promise { + return new Package(this.projectRootPath, { fileSystem: this.fs }).name(); } - /** - * Exponent sdk version that maps to the current react-native version - * If react native version is not supported it returns null. - */ - private exponentSdk(showProgress: boolean = false): Q.Promise { - if (showProgress) Log.logString("..."); - if (this.expSdkVersion) { - return Q(this.expSdkVersion); - } - return this.readFromExpJson("sdkVersion") - .then((sdkVersion) => { - if (showProgress) Log.logString("."); - if (sdkVersion) { - this.expSdkVersion = sdkVersion; - return this.expSdkVersion; + private getExpConfig(): Q.Promise { + return this.readExpJson() + .catch(err => { + if (err.code === "ENOENT") { + return this.readAppJson(); } - let reactNativeProjectHelper = new ReactNativeProjectHelper(this.projectRootPath); - return reactNativeProjectHelper.getReactNativeVersion() - .then(version => { - if (showProgress) Log.logString("."); - return XDL.mapVersion(version) - .then(exponentVersion => { - this.expSdkVersion = exponentVersion; - return this.expSdkVersion; - }); - }); - }); - } - /** - * Returns the specified setting from exp.json if it exists - */ - private readFromExpJson(setting: string): Q.Promise { - const expJsonPath = this.pathToFileInWorkspace(EXP_JSON); - return this.fileSystem.exists(expJsonPath) - .then((exists: boolean) => { - if (!exists) { - return null; - } - return this.fileSystem.readFile(expJsonPath, "utf-8") - .then(function (jsonContents: string): Q.Promise { - return JSON.parse(stripJsonComments(jsonContents))[setting]; - }); + return err; }); } - /** - * Looks at the _from attribute in the package json of the react-native dependency - * to figure out if it's using exponent. - */ - private usingReactNativeExponent(showProgress: boolean = false): Q.Promise { - if (showProgress) Log.logString("..."); - if (this.dependencyPackage !== ReactNativePackageStatus.UNKNOWN) { - return Q(this.dependencyPackage === ReactNativePackageStatus.EXPONENT_PACKAGE); - } - // Look for the package.json of the dependecy - const pathToReactNativePackageJson = path.resolve(this.projectRootPath, "node_modules", "react-native", "package.json"); - return this.fileSystem.readFile(pathToReactNativePackageJson, "utf-8") - .then(jsonContents => { - const packageJson = JSON.parse(jsonContents); - const isExp = /\bexponentjs\/react-native\b/.test(packageJson._from); - this.dependencyPackage = isExp ? ReactNativePackageStatus.EXPONENT_PACKAGE : ReactNativePackageStatus.FACEBOOK_PACKAGE; - if (showProgress) Log.logString("."); - return isExp; - }).catch(() => { - if (showProgress) Log.logString("."); - // Not in a react-native project - return false; - }); + private getFromExpConfig(key: string): Q.Promise { + return this.getExpConfig() + .then((config: ExpConfig) => config[key]); } /** - * Name of the file (we assume it lives in the workspace root) that should be used as entrypoint. - * e.g. index.ios.js + * Returns the specified setting from exp.json if it exists */ - private entrypoint(): Q.Promise { - if (this.entrypointFilename) { - return Q(this.entrypointFilename); - } - return this.readVscodeExponentSettingFile() - .then((settingsJson) => { - // Let's load both to memory to make sure we are not reading from memory next time we query for this. - this.entrypointFilename = settingsJson.entryPointFilename; - this.entrypointComponentName = settingsJson.entryPointComponent; - return this.entrypointFilename; + private readExpJson(): Q.Promise { + const expJsonPath = this.pathToFileInWorkspace(EXP_JSON); + return this.fs.readFile(expJsonPath) + .then(content => { + return JSON.parse(stripJSONComments(content)); }); } - /** - * Name of the component used as an entrypoint for the app. - */ - private entrypointComponent(): Q.Promise { - if (this.entrypointComponentName) { - return Q(this.entrypointComponentName); - } - return this.readVscodeExponentSettingFile() - .then((settingsJson) => { - // Let's load both to memory to make sure we are not reading from memory next time we query for this. - this.entrypointComponentName = settingsJson.entryPointComponent; - this.entrypointFilename = settingsJson.entrypointFilename; - return this.entrypointComponentName; + private readAppJson(): Q.Promise { + const appJsonPath = this.pathToFileInWorkspace(APP_JSON); + return this.fs.readFile(appJsonPath) + .then(content => { + return JSON.parse(stripJSONComments(content)).expo; }); } - /** - * Path to a given file inside the .vscode directory - */ - private dotvscodePath(filename: string): string { - return path.join(this.workspaceRootPath, ".vscode", filename); + private writeAppJson(content: ExpConfig): Q.Promise { + const appJsonPath = this.pathToFileInWorkspace(APP_JSON); + return this.fs.writeFile(appJsonPath, JSON.stringify({ + expo: content + }, null, 2)); } /** @@ -399,11 +149,26 @@ AppRegistry.registerRunnable('main', function(appParameters) { return path.join(this.projectRootPath, filename); } - /** - * Name specified on user's package.json - */ - private getPackageName(): Q.Promise { - return new Package(this.projectRootPath, { fileSystem: this.fileSystem }).name(); + private isExpoApp(showProgress: boolean = false): Q.Promise { + Log.logString("Checking if this is Expo app."); + if (showProgress) { + Log.logString("..."); + } + + const packageJsonPath = this.pathToFileInWorkspace("package.json"); + return this.fs.readFile(packageJsonPath) + .then(content => { + const packageJson = JSON.parse(content); + const isExp = packageJson.dependencies && !!packageJson.dependencies.expo || false; + if (showProgress) Log.logString("."); + return isExp; + }).catch(() => { + if (showProgress) { + Log.logString("."); + } + // Not in a react-native project + return false; + }); } /** @@ -412,9 +177,7 @@ AppRegistry.registerRunnable('main', function(appParameters) { private lazilyInitialize(): void { if (!this.hasInitialized) { this.hasInitialized = true; - this.fileSystem = new FileSystem(); - this.commandExecutor = new CommandExecutor(this.projectRootPath); - this.dependencyPackage = ReactNativePackageStatus.UNKNOWN; + this.fs = new FileSystem(); XDL.configReactNativeVersionWargnings(); XDL.attachLoggerStream(this.projectRootPath, { @@ -433,4 +196,4 @@ AppRegistry.registerRunnable('main', function(appParameters) { }); } } -} +} \ No newline at end of file diff --git a/src/common/exponent/xdlInterface.ts b/src/common/exponent/xdlInterface.ts index 619c72bbc..484b64515 100644 --- a/src/common/exponent/xdlInterface.ts +++ b/src/common/exponent/xdlInterface.ts @@ -9,7 +9,11 @@ import * as XDLPackage from "xdl"; import * as path from "path"; import * as Q from "q"; -const XDL_VERSION = "36.1.0"; +const EXPO_DEPS: string[] = [ + "xdl", + "@expo/ngrok", // devDependencies for xdl +]; + let xdlPackage: Q.Promise; function getPackage(): Q.Promise { @@ -31,7 +35,7 @@ function getPackage(): Q.Promise { } let commandExecutor = new CommandExecutor(); xdlPackage = commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), - ["install", `xdl@${XDL_VERSION}`, "--verbose"], + ["install", EXPO_DEPS.join(", "), "--verbose"], { verbosity: CommandVerbosity.PROGRESS, cwd: path.dirname(require.resolve("../../../"))}) .then((): typeof XDLPackage => { @@ -49,68 +53,68 @@ export function configReactNativeVersionWargnings(): Q.Promise { }); } -export function attachLoggerStream(rootPath: string, options?: XDLPackage.IBunyanStream): Q.Promise { +export function attachLoggerStream(rootPath: string, options?: XDLPackage.IBunyanStream | any): Q.Promise { return getPackage() .then((xdl) => xdl.ProjectUtils.attachLoggerStream(rootPath, options)); } -export function supportedVersions(): Q.Promise> { +export function supportedVersions(): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Versions.facebookReactNativeVersionsAsync()); } export function currentUser(): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.User.getCurrentUserAsync()); } export function login(username: string, password: string): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.User.loginAsync("user-pass", { username: username, password: password })); } export function mapVersion(reactNativeVersion: string): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Versions.facebookReactNativeVersionToExpoVersionAsync(reactNativeVersion)); } export function publish(projectRoot: string, options?: XDLPackage.IPublishOptions): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.publishAsync(projectRoot, options)); } export function setOptions(projectRoot: string, options?: XDLPackage.IOptions): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.setOptionsAsync(projectRoot, options)); } export function startExponentServer(projectRoot: string): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.startExpoServerAsync(projectRoot)); } export function startTunnels(projectRoot: string): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.startTunnelsAsync(projectRoot)); } export function getUrl(projectRoot: string, options?: XDLPackage.IUrlOptions): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.getUrlAsync(projectRoot, options)); } export function stopAll(projectRoot: string): Q.Promise { return getPackage() - .then((xdl) => + .then((xdl: any) => xdl.Project.stopAsync(projectRoot)); } diff --git a/src/common/packager.ts b/src/common/packager.ts index af48b1b0e..d10ee521b 100644 --- a/src/common/packager.ts +++ b/src/common/packager.ts @@ -79,15 +79,19 @@ export class Packager { }) .then(() => XDL.setOptions(this.projectPath, { packagerPort: port }) - ).then(() => + ) + .then(() => XDL.startExponentServer(this.projectPath) - ).then(() => + ) + .then(() => XDL.startTunnels(this.projectPath) - ).then(() => + ) + .then(() => XDL.getUrl(this.projectPath, { dev: true, minify: false }) ).then(exponentUrl => { return "exp://" + url.parse(exponentUrl).host; - }).catch(reason => { + }) + .catch(reason => { return Q.reject(reason); }); } @@ -191,28 +195,36 @@ export class Packager { executedStartPackagerCmd = true; return this.monkeyPatchOpnForRNPackager() .then(() => { - let args = ["--port", port.toString()]; + let args: any = ["--port", port.toString()]; if (resetCache) { args = args.concat("--resetCache"); } if (runAs !== PackagerRunAs.EXPONENT) { - return args; + return args; } - args = args.concat(["--root", path.relative(this.projectPath, path.resolve(this.workspacePath, ".vscode"))]); - let helper = new ExponentHelper(this.workspacePath, this.projectPath); - return helper.getExponentPackagerOptions() - .then((options) => { - return Object.keys(options).reduce((args, key) => { - return args.concat(["--" + key, options[key]]); - }, args); - }) - .catch(() => { - Log.logWarning("Couldn't read packager's options from exp.json, continue..."); - return args; - }); - }) + args.push("--projectRoots", this.projectPath); + + let helper = new ExponentHelper(this.projectPath); + return helper.getExpPackagerOptions() + .then((options) => { + Object.keys(options).forEach(key => { + args.push(`--${key}`, options[key]); + }); + + // Patch for CRNA + if (args.indexOf("--assetExts") === -1) { + args.push("--assetExts", ["ttf"]); + } + + return args; + }) + .catch(() => { + Log.logWarning("Couldn't read packager's options from exp.json, continue..."); + return args; + }); + }) .then((args) => { let reactNativeProjectHelper = new ReactNativeProjectHelper(this.projectPath); reactNativeProjectHelper.getReactNativeVersion().then(version => { @@ -238,8 +250,8 @@ export class Packager { packagerSpawnResult.outcome.done(() => { }, () => { }); /* Q prints a warning if we don't call .done(). We ignore all outcome errors */ return packagerSpawnResult.startup; + }); }); - }); } }) .then(() => diff --git a/src/extension/commandPaletteHandler.ts b/src/extension/commandPaletteHandler.ts index 7de3980b2..c01b48a1f 100644 --- a/src/extension/commandPaletteHandler.ts +++ b/src/extension/commandPaletteHandler.ts @@ -40,9 +40,8 @@ export class CommandPaletteHandler { return this.reactNativePackager.stop(); } }) - ).then(() => - this.exponentHelper.configureReactNativeEnvironment() - ).then(() => this.runStartPackagerCommandAndUpdateStatus()); + ) + .then(() => this.runStartPackagerCommandAndUpdateStatus()); } /** diff --git a/src/extension/extensionServer.ts b/src/extension/extensionServer.ts index 4dc993db3..271f0b0b6 100644 --- a/src/extension/extensionServer.ts +++ b/src/extension/extensionServer.ts @@ -103,9 +103,8 @@ export class ExtensionServer implements vscode.Disposable { Log.logMessage("Attaching to running React Native packager"); } - }).then(() => - this.exponentHelper.configureReactNativeEnvironment() - ).then(() => { + }) + .then(() => { const portToUse = ConfigurationReader.readIntWithDefaultSync(port, SettingsHelper.getPackagerPort()); return this.reactNativePackager.startAsReactNative(portToUse); }) diff --git a/src/extension/rn-extension.ts b/src/extension/rn-extension.ts index 1ce5f970b..b8eea753d 100644 --- a/src/extension/rn-extension.ts +++ b/src/extension/rn-extension.ts @@ -44,7 +44,7 @@ const projectRootPath = SettingsHelper.getReactNativeProjectRoot(); const workspaceRootPath = vscode.workspace.rootPath; const globalPackager = new Packager(workspaceRootPath, projectRootPath); const packagerStatusIndicator = new PackagerStatusIndicator(); -const globalExponentHelper = new ExponentHelper(workspaceRootPath, projectRootPath); +const globalExponentHelper = new ExponentHelper(projectRootPath); const commandPaletteHandler = new CommandPaletteHandler(projectRootPath, globalPackager, packagerStatusIndicator, globalExponentHelper); const outputChannelLogger = new DelayedOutputChannelLogger("React-Native"); From 4f7b30bcc1df2c472e28b5d22438b013f8edd931 Mon Sep 17 00:00:00 2001 From: Artem Egorov Date: Wed, 19 Jul 2017 14:40:04 +0300 Subject: [PATCH 2/4] Code refactoring for better Expo support --- src/common/exponent/exponentHelper.d.ts | 19 ++-- src/common/exponent/exponentHelper.ts | 136 ++++++++++++++++++++---- src/common/packager.ts | 2 +- 3 files changed, 131 insertions(+), 26 deletions(-) diff --git a/src/common/exponent/exponentHelper.d.ts b/src/common/exponent/exponentHelper.d.ts index 6b5430390..133e5ad7e 100644 --- a/src/common/exponent/exponentHelper.d.ts +++ b/src/common/exponent/exponentHelper.d.ts @@ -1,14 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. +declare class AppJson { + public name: string; + public displayName?: string; + public expo?: ExpConfig; +} + declare class ExpConfig { - name: string; - slug: string; - sdkVersion: string; - version?: string; - packagerOpts?: ExpConfigPackager; + public name: string; + public slug: string; + public sdkVersion: string; + public version?: string; + public entryPoint?: string; + public packagerOpts?: ExpConfigPackager; } declare class ExpConfigPackager { - assetExts? : string[]; + public assetExts?: string[]; } \ No newline at end of file diff --git a/src/common/exponent/exponentHelper.ts b/src/common/exponent/exponentHelper.ts index e53cddb9a..96fbf5717 100644 --- a/src/common/exponent/exponentHelper.ts +++ b/src/common/exponent/exponentHelper.ts @@ -7,14 +7,22 @@ import * as path from "path"; import * as Q from "q"; import stripJSONComments = require("strip-json-comments"); -import {Package} from "../node/package"; import * as XDL from "./xdlInterface"; +import { Package } from "../node/package"; +import { ReactNativeProjectHelper } from "../reactNativeProjectHelper"; import { FileSystem } from "../node/fileSystem"; import { Log } from "../log/log"; const APP_JSON = "app.json"; const EXP_JSON = "exp.json"; +const EXPONENT_INDEX = "./.vscode/exponentIndex.js"; +const DEFAULT_EXPONENT_INDEX = "index.js"; +const DEFAULT_IOS_INDEX = "index.ios.js"; +const DEFAULT_ANDROID_INDEX = "index.android.js"; + +const DBL_SLASHES = /\\/g; + export class ExponentHelper { private projectRootPath: string; private fs: FileSystem; @@ -36,7 +44,7 @@ export class ExponentHelper { .then(isExpo => { Log.logString(".\n"); - return this.patchAppJson(); + return this.patchAppJson(isExpo); }); } @@ -72,27 +80,115 @@ export class ExponentHelper { .then(opts => opts || {}); } - private patchAppJson(): Q.Promise { + private createExpoEntry(name: string): Q.Promise { + this.lazilyInitialize(); + return this.detectEntry() + .then((entryPoint: string) => { + const content = this.generateFileContent(name, entryPoint); + return this.fs.writeFile(this.pathToFileInWorkspace(EXPONENT_INDEX), content); + }); + + } + + private detectEntry(): Q.Promise { + this.lazilyInitialize(); + return Q.all([ + this.fs.exists(this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX)), + this.fs.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)), + this.fs.exists(this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX)), + ]) + .spread((expo: boolean, ios: boolean, android: boolean): string => { + return expo ? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX) : + ios ? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX) : + this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX); + }); + } + + private generateFileContent(name: string, entryPoint: string): string { + return `// This file is automatically generated by VS Code +// Please do not modify it manually. All changes will be lost. +var React = require('${this.pathToFileInWorkspace("/node_modules/react")}'); +var { Component } = React; +var ReactNative = require('${this.pathToFileInWorkspace("/node_modules/react-native")}'); +var { AppRegistry } = ReactNative; +var entryPoint = require('${entryPoint}'); +AppRegistry.registerRunnable('main', function(appParameters) { + AppRegistry.runApplication('${name}', appParameters); +});`; + } + + private patchAppJson(isExpo: boolean = true): Q.Promise { return this.readAppJson() - .then((config: ExpConfig) => { - if (!config.name || !config.slug) { + .then((config: AppJson) => { + let expoConfig = (config.expo || {}); + if (!expoConfig.name || !expoConfig.slug) { return this.getPackageName() - .then(name => { - config.slug = config.slug || name.replace(" ", "-"); - config.name = config.name || name; + .then((name: string) => { + expoConfig.slug = expoConfig.slug || config.name || name.replace(" ", "-"); + expoConfig.name = expoConfig.name || config.name || name; + config.expo = expoConfig; return config; }); } - return Q.resolve(null); + return config; }) - .then((config: ExpConfig) => { + .then((config: AppJson) => { + if (!config.expo.sdkVersion) { + return this.exponentSdk(true) + .then(sdkVersion => { + config.expo.sdkVersion = sdkVersion; + return config; + }); + } + return config; + }) + .then((config: AppJson) => { + if (!isExpo) { + config.expo.entryPoint = this.pathToFileInWorkspace(EXPONENT_INDEX); + } + + return config; + }) + .then((config: AppJson) => { if (config) { return this.writeAppJson(config); } + }) + .then((config: AppJson) => { + if (!isExpo) { + return this.createExpoEntry(config.expo.name); + } }); }; + /** + * Exponent sdk version that maps to the current react-native version + * If react native version is not supported it returns null. + */ + private exponentSdk(showProgress: boolean = false): Q.Promise { + if (showProgress) { + Log.logString("..."); + } + + let reactNativeProjectHelper = new ReactNativeProjectHelper(this.projectRootPath); + return reactNativeProjectHelper.getReactNativeVersion() + .then(version => { + if (showProgress) Log.logString("."); + return XDL.mapVersion(version) + .then(sdkVersion => { + if (!sdkVersion) { + return XDL.supportedVersions() + .then((versions) => { + return Q.reject(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`)); + }); + } + return sdkVersion; + }); + }); + } + + /** * Name specified on user's package.json */ @@ -104,7 +200,10 @@ export class ExponentHelper { return this.readExpJson() .catch(err => { if (err.code === "ENOENT") { - return this.readAppJson(); + return this.readAppJson() + .then((config: AppJson) => { + return config.expo || {}; + }); } return err; @@ -127,29 +226,28 @@ export class ExponentHelper { }); } - private readAppJson(): Q.Promise { + private readAppJson(): Q.Promise { const appJsonPath = this.pathToFileInWorkspace(APP_JSON); return this.fs.readFile(appJsonPath) .then(content => { - return JSON.parse(stripJSONComments(content)).expo; + return JSON.parse(stripJSONComments(content)); }); } - private writeAppJson(content: ExpConfig): Q.Promise { + private writeAppJson(config: AppJson): Q.Promise { const appJsonPath = this.pathToFileInWorkspace(APP_JSON); - return this.fs.writeFile(appJsonPath, JSON.stringify({ - expo: content - }, null, 2)); + return this.fs.writeFile(appJsonPath, JSON.stringify(config, null, 2)) + .then(() => config); } /** * Path to a given file from the workspace root */ private pathToFileInWorkspace(filename: string): string { - return path.join(this.projectRootPath, filename); + return path.join(this.projectRootPath, filename).replace(DBL_SLASHES, "/"); } - private isExpoApp(showProgress: boolean = false): Q.Promise { + private isExpoApp(showProgress: boolean = false): Q.Promise { Log.logString("Checking if this is Expo app."); if (showProgress) { Log.logString("..."); diff --git a/src/common/packager.ts b/src/common/packager.ts index d10ee521b..94e207e34 100644 --- a/src/common/packager.ts +++ b/src/common/packager.ts @@ -204,7 +204,7 @@ export class Packager { return args; } - args.push("--projectRoots", this.projectPath); + args.push("--root", path.resolve(this.projectPath + "/.vscode/")); let helper = new ExponentHelper(this.projectPath); return helper.getExpPackagerOptions() From 4cabcbee0657c3d4590e5c32f30faa1be5e77a13 Mon Sep 17 00:00:00 2001 From: Artem Egorov Date: Tue, 25 Jul 2017 15:50:06 +0300 Subject: [PATCH 3/4] Fix issue after review --- src/common/exponent/exponentHelper.ts | 26 +++++++++++++++++++------- src/common/exponent/xdlInterface.ts | 4 ++-- src/common/packager.ts | 4 ++-- src/extension/rn-extension.ts | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/common/exponent/exponentHelper.ts b/src/common/exponent/exponentHelper.ts index 96fbf5717..26654cdb7 100644 --- a/src/common/exponent/exponentHelper.ts +++ b/src/common/exponent/exponentHelper.ts @@ -5,18 +5,17 @@ import * as path from "path"; import * as Q from "q"; -import stripJSONComments = require("strip-json-comments"); - import * as XDL from "./xdlInterface"; import { Package } from "../node/package"; import { ReactNativeProjectHelper } from "../reactNativeProjectHelper"; import { FileSystem } from "../node/fileSystem"; import { Log } from "../log/log"; +import stripJSONComments = require("strip-json-comments"); const APP_JSON = "app.json"; const EXP_JSON = "exp.json"; -const EXPONENT_INDEX = "./.vscode/exponentIndex.js"; +const EXPONENT_INDEX = "exponentIndex.js"; const DEFAULT_EXPONENT_INDEX = "index.js"; const DEFAULT_IOS_INDEX = "index.ios.js"; const DEFAULT_ANDROID_INDEX = "index.android.js"; @@ -24,11 +23,13 @@ const DEFAULT_ANDROID_INDEX = "index.android.js"; const DBL_SLASHES = /\\/g; export class ExponentHelper { + private workspaceRootPath: string; private projectRootPath: string; private fs: FileSystem; private hasInitialized: boolean; - public constructor(projectRootPath: string) { + public constructor(workspaceRootPath: string, projectRootPath: string) { + this.workspaceRootPath = workspaceRootPath; this.projectRootPath = projectRootPath; this.hasInitialized = false; // Constructor is slim by design. This is to add as less computation as possible @@ -80,12 +81,19 @@ export class ExponentHelper { .then(opts => opts || {}); } + /** + * Path to a given file inside the .vscode directory + */ + private dotvscodePath(filename: string): string { + return path.join(this.workspaceRootPath, ".vscode", filename); + } + private createExpoEntry(name: string): Q.Promise { this.lazilyInitialize(); return this.detectEntry() .then((entryPoint: string) => { const content = this.generateFileContent(name, entryPoint); - return this.fs.writeFile(this.pathToFileInWorkspace(EXPONENT_INDEX), content); + return this.fs.writeFile(this.dotvscodePath(EXPONENT_INDEX), content); }); } @@ -97,7 +105,7 @@ export class ExponentHelper { this.fs.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX)), this.fs.exists(this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX)), ]) - .spread((expo: boolean, ios: boolean, android: boolean): string => { + .spread((expo: boolean, ios: boolean): string => { return expo ? this.pathToFileInWorkspace(DEFAULT_EXPONENT_INDEX) : ios ? this.pathToFileInWorkspace(DEFAULT_IOS_INDEX) : this.pathToFileInWorkspace(DEFAULT_ANDROID_INDEX); @@ -119,6 +127,10 @@ AppRegistry.registerRunnable('main', function(appParameters) { private patchAppJson(isExpo: boolean = true): Q.Promise { return this.readAppJson() + .catch(() => { + // if app.json doesn't exist but it's ok, we will create it + return {}; + }) .then((config: AppJson) => { let expoConfig = (config.expo || {}); if (!expoConfig.name || !expoConfig.slug) { @@ -145,7 +157,7 @@ AppRegistry.registerRunnable('main', function(appParameters) { }) .then((config: AppJson) => { if (!isExpo) { - config.expo.entryPoint = this.pathToFileInWorkspace(EXPONENT_INDEX); + config.expo.entryPoint = this.dotvscodePath(EXPONENT_INDEX); } return config; diff --git a/src/common/exponent/xdlInterface.ts b/src/common/exponent/xdlInterface.ts index 484b64515..20f28169d 100644 --- a/src/common/exponent/xdlInterface.ts +++ b/src/common/exponent/xdlInterface.ts @@ -35,7 +35,7 @@ function getPackage(): Q.Promise { } let commandExecutor = new CommandExecutor(); xdlPackage = commandExecutor.spawnWithProgress(HostPlatform.getNpmCliCommand("npm"), - ["install", EXPO_DEPS.join(", "), "--verbose"], + ["install", EXPO_DEPS.join(" "), "--verbose"], { verbosity: CommandVerbosity.PROGRESS, cwd: path.dirname(require.resolve("../../../"))}) .then((): typeof XDLPackage => { @@ -103,7 +103,7 @@ export function startExponentServer(projectRoot: string): Q.Promise { export function startTunnels(projectRoot: string): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.startTunnelsAsync(projectRoot)); } diff --git a/src/common/packager.ts b/src/common/packager.ts index 94e207e34..b754ed5d3 100644 --- a/src/common/packager.ts +++ b/src/common/packager.ts @@ -204,9 +204,9 @@ export class Packager { return args; } - args.push("--root", path.resolve(this.projectPath + "/.vscode/")); + args.push("--root", path.relative(this.projectPath, path.resolve(this.workspacePath, ".vscode"))); - let helper = new ExponentHelper(this.projectPath); + let helper = new ExponentHelper(this.workspacePath, this.projectPath); return helper.getExpPackagerOptions() .then((options) => { Object.keys(options).forEach(key => { diff --git a/src/extension/rn-extension.ts b/src/extension/rn-extension.ts index b8eea753d..1ce5f970b 100644 --- a/src/extension/rn-extension.ts +++ b/src/extension/rn-extension.ts @@ -44,7 +44,7 @@ const projectRootPath = SettingsHelper.getReactNativeProjectRoot(); const workspaceRootPath = vscode.workspace.rootPath; const globalPackager = new Packager(workspaceRootPath, projectRootPath); const packagerStatusIndicator = new PackagerStatusIndicator(); -const globalExponentHelper = new ExponentHelper(projectRootPath); +const globalExponentHelper = new ExponentHelper(workspaceRootPath, projectRootPath); const commandPaletteHandler = new CommandPaletteHandler(projectRootPath, globalPackager, packagerStatusIndicator, globalExponentHelper); const outputChannelLogger = new DelayedOutputChannelLogger("React-Native"); From 87f1c60d56cc985a56e8dcc75aeaeecf69a14572 Mon Sep 17 00:00:00 2001 From: Artem Egorov Date: Tue, 25 Jul 2017 17:21:51 +0300 Subject: [PATCH 4/4] Fix issue after review --- src/common/commandExecutor.ts | 9 ++++----- src/common/exponent/exponentHelper.ts | 4 ++-- src/common/exponent/xdlInterface.ts | 18 +++++++++--------- src/common/packager.ts | 8 ++++---- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/common/commandExecutor.ts b/src/common/commandExecutor.ts index 335e2675e..23b607f12 100644 --- a/src/common/commandExecutor.ts +++ b/src/common/commandExecutor.ts @@ -33,8 +33,7 @@ export enum CommandStatus { export class CommandExecutor { - private static ReactNativeGlobal = "react-native"; - private static ReactNativeCLI = "node_modules/react-native/local-cli/cli.js"; + private static ReactNativeCommand = "react-native"; private static ReactNativeVersionCommand = "-v"; private currentWorkingDirectory: string; private childProcess = new Node.ChildProcess(); @@ -78,7 +77,7 @@ export class CommandExecutor { */ public getReactNativeVersion(): Q.Promise { let deferred = Q.defer(); - const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeGlobal); + const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeCommand); let output = ""; const result = this.childProcess.spawn(reactCommand, @@ -122,8 +121,8 @@ export class CommandExecutor { * Executes a react native command and waits for its completion. */ public spawnReactCommand(command: string, args?: string[], options: Options = {}): ISpawnResult { - const reactCommand = CommandExecutor.ReactNativeCLI; - return this.spawnChildProcess("node", [reactCommand, command, ...args], options); + const reactCommand = HostPlatform.getNpmCliCommand(CommandExecutor.ReactNativeCommand); + return this.spawnChildProcess(reactCommand, [command, ...args], options); } /** diff --git a/src/common/exponent/exponentHelper.ts b/src/common/exponent/exponentHelper.ts index 26654cdb7..0be797f34 100644 --- a/src/common/exponent/exponentHelper.ts +++ b/src/common/exponent/exponentHelper.ts @@ -77,7 +77,7 @@ export class ExponentHelper { public getExpPackagerOptions(): Q.Promise { this.lazilyInitialize(); - return this.getFromExpConfig("packagerOpts") + return this.getFromExpConfig("packagerOpts") .then(opts => opts || {}); } @@ -222,7 +222,7 @@ AppRegistry.registerRunnable('main', function(appParameters) { }); } - private getFromExpConfig(key: string): Q.Promise { + private getFromExpConfig(key: string): Q.Promise { return this.getExpConfig() .then((config: ExpConfig) => config[key]); } diff --git a/src/common/exponent/xdlInterface.ts b/src/common/exponent/xdlInterface.ts index 20f28169d..3d8d1d225 100644 --- a/src/common/exponent/xdlInterface.ts +++ b/src/common/exponent/xdlInterface.ts @@ -61,43 +61,43 @@ export function attachLoggerStream(rootPath: string, options?: XDLPackage.IBunya export function supportedVersions(): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Versions.facebookReactNativeVersionsAsync()); } export function currentUser(): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.User.getCurrentUserAsync()); } export function login(username: string, password: string): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.User.loginAsync("user-pass", { username: username, password: password })); } export function mapVersion(reactNativeVersion: string): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Versions.facebookReactNativeVersionToExpoVersionAsync(reactNativeVersion)); } export function publish(projectRoot: string, options?: XDLPackage.IPublishOptions): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.publishAsync(projectRoot, options)); } export function setOptions(projectRoot: string, options?: XDLPackage.IOptions): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.setOptionsAsync(projectRoot, options)); } export function startExponentServer(projectRoot: string): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.startExpoServerAsync(projectRoot)); } @@ -109,12 +109,12 @@ export function startTunnels(projectRoot: string): Q.Promise { export function getUrl(projectRoot: string, options?: XDLPackage.IUrlOptions): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.getUrlAsync(projectRoot, options)); } export function stopAll(projectRoot: string): Q.Promise { return getPackage() - .then((xdl: any) => + .then((xdl) => xdl.Project.stopAsync(projectRoot)); } diff --git a/src/common/packager.ts b/src/common/packager.ts index b754ed5d3..c41b5b0e2 100644 --- a/src/common/packager.ts +++ b/src/common/packager.ts @@ -195,7 +195,7 @@ export class Packager { executedStartPackagerCmd = true; return this.monkeyPatchOpnForRNPackager() .then(() => { - let args: any = ["--port", port.toString()]; + let args: string[] = ["--port", port.toString()]; if (resetCache) { args = args.concat("--resetCache"); } @@ -208,14 +208,14 @@ export class Packager { let helper = new ExponentHelper(this.workspacePath, this.projectPath); return helper.getExpPackagerOptions() - .then((options) => { + .then((options: ExpConfigPackager) => { Object.keys(options).forEach(key => { - args.push(`--${key}`, options[key]); + args.concat([`--${key}`, options[key]]); }); // Patch for CRNA if (args.indexOf("--assetExts") === -1) { - args.push("--assetExts", ["ttf"]); + args.push("--assetExts", "ttf"); } return args;