diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0a1d8169..b72604e99f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ NativeScript CLI Changelog ================ +1.1.2 (2015, July 2) +== + +### New + +* [Implemented #600](https://github.com/NativeScript/nativescript-cli/issues/600): Added the `$ tns init` command. You can use it to initialize a NativeScript project for development. The command recreates or updates the `package.json` file of the project. You can then run `$ tns install` to install the platforms and dependencies described in the `package.json` file. +* [Implemented #587](https://github.com/NativeScript/nativescript-cli/issues/587): Added the `$ tns install` command. You can use it to quickly install all platforms and dependencies described in a `package.json` file. + +### Fixed + +* [Fixed #606](https://github.com/NativeScript/nativescript-cli/issues/606): The NativeScript CLI lets you run ``-based commands on operating systems which do not support development for this platform. For example, you can run `$ tns build ios` on a Windows system. +* [Fixed #601](https://github.com/NativeScript/nativescript-cli/issues/601): The NativeScript CLI does not treat the dependencies of devDependencies as devDependencies. +* [Fixed #599](https://github.com/NativeScript/nativescript-cli/issues/599): The NativeScript CLI always creates a `tmp` directory in the current directory when you run any command and modifies the `package.json` file for the project. +* [Fixed #584](https://github.com/NativeScript/nativescript-cli/issues/584): The NativeScript CLI references the files in the project with their absolute paths instead of their relative paths. This might cause the project to stop working when transfered to another location and might cause issues with your application packages. +* [Fixed #578](https://github.com/NativeScript/nativescript-cli/issues/578): Platform-specific files in NativeScript plugins are not processed correctly. The NativeScript CLI copies them to the correct platform-specific directory but does not rename them correctly. +* [Fixed #520](https://github.com/NativeScript/nativescript-cli/issues/520): iOS resources from the `App_Resources` folder are not included in the native Xcode project and are not accessible in the application package. + 1.1.1 (2015, June 17) == diff --git a/bin/nativescript.js b/bin/nativescript.js old mode 100644 new mode 100755 diff --git a/docs/man_pages/index.md b/docs/man_pages/index.md index 50d4c7b827..3cbf625ba6 100644 --- a/docs/man_pages/index.md +++ b/docs/man_pages/index.md @@ -28,7 +28,8 @@ Command | Description [emulate ``](project/testing/emulate.html) | Deploys the project in the native emulator for the selected target platform. [run ``](project/testing/run.html) | Runs your project on a connected device or in the native emulator, if configured. [debug ``](project/testing/debug.html) | Debugs your project on a connected device. -[plugin](plugin.html) | Lists all installed plugins for your project or lets you manage the plugins for your project. +[install](project/configuration/install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. +[plugin](lib-management/plugin.html) | Lets you manage the plugins for your project. ## Device Commands Command | Description diff --git a/docs/man_pages/project/configuration/install.md b/docs/man_pages/project/configuration/install.md new file mode 100644 index 0000000000..83ba723ea8 --- /dev/null +++ b/docs/man_pages/project/configuration/install.md @@ -0,0 +1,27 @@ +install +========== + +Usage | Synopsis +---|--- +General | `$ tns install [--path]` + +Installs all platforms and dependencies described in the `package.json` file in the current directory. + +<% if(isHtml) { %> +The `package.json` file must be a valid `package.json` describing the configuration of a NativeScript project. If missing or corrupted, you can recreate the file by running `$ tns init` in the directory of a NativeScript project. +<% } %> + +### Options +* `--path` - Specifies the directory which contains the `package.json` file, if different from the current directory. + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[platform add](platform-add.html) | Configures the current project to target the selected platform. +[platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. +[platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform. +[platform](platform.html) | Lists all platforms that the project currently targets. +[prepare](prepare.html) | Copies common and relevant platform-specific content from the app directory to the subdirectory for the selected target platform in the platforms directory. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/platform-add.md b/docs/man_pages/project/configuration/platform-add.md index 307471cd3e..8e7458f310 100644 --- a/docs/man_pages/project/configuration/platform-add.md +++ b/docs/man_pages/project/configuration/platform-add.md @@ -32,6 +32,7 @@ To list only experimental versions for iOS, run `$ npm view tns-ios dist-tags` Command | Description ----------|---------- +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. [platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. [platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform. [platform](platform.html) | Lists all platforms that the project currently targets. diff --git a/docs/man_pages/project/configuration/platform-remove.md b/docs/man_pages/project/configuration/platform-remove.md index 99d9a6b4a3..021f43e612 100644 --- a/docs/man_pages/project/configuration/platform-remove.md +++ b/docs/man_pages/project/configuration/platform-remove.md @@ -24,6 +24,7 @@ Removes the selected platform from the platforms that the project currently targ Command | Description ----------|---------- +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. [platform add](platform-add.html) | Configures the current project to target the selected platform. [platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform. [platform](platform.html) | Lists all platforms that the project currently targets. diff --git a/docs/man_pages/project/configuration/platform-update.md b/docs/man_pages/project/configuration/platform-update.md index ef43d2c102..79381aca70 100644 --- a/docs/man_pages/project/configuration/platform-update.md +++ b/docs/man_pages/project/configuration/platform-update.md @@ -29,6 +29,7 @@ To list only experimental versions for ios, run `$ npm view tns-ios dist-tags` Command | Description ----------|---------- +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. [platform add](platform-add.html) | Configures the current project to target the selected platform. [platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. [platform](platform.html) | Lists all platforms that the project currently targets. diff --git a/docs/man_pages/project/configuration/platform.md b/docs/man_pages/project/configuration/platform.md index ddb6d4730d..0412312f95 100644 --- a/docs/man_pages/project/configuration/platform.md +++ b/docs/man_pages/project/configuration/platform.md @@ -12,6 +12,7 @@ Lists all platforms that the project currently targets. You can build and deploy Command | Description ----------|---------- +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. [platform add](platform-add.html) | Configures the current project to target the selected platform. [platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. [platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform. diff --git a/docs/man_pages/project/configuration/prepare.md b/docs/man_pages/project/configuration/prepare.md index bfca2d296c..ad076725d1 100644 --- a/docs/man_pages/project/configuration/prepare.md +++ b/docs/man_pages/project/configuration/prepare.md @@ -23,6 +23,7 @@ in the `platforms` directory. This lets you build the project with the SDK for t Command | Description ----------|---------- +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. [platform add](platform-add.html) | Configures the current project to target the selected platform. [platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. [platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform. diff --git a/docs/man_pages/project/creation/create.md b/docs/man_pages/project/creation/create.md index 152788c977..39e8c71bf0 100644 --- a/docs/man_pages/project/creation/create.md +++ b/docs/man_pages/project/creation/create.md @@ -16,4 +16,13 @@ Creates a new project for native development with NativeScript from the default * `` is the name of project. The specified name must meet the requirements of all platforms that you want to target. <% if(isConsole) { %>For more information about the `` requirements, run `$ tns help create`<% } %><% if(isHtml) { %>For projects that target Android, you can use uppercase or lowercase letters, numbers, and underscores. The name must start with a letter. For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens.<% } %> * `` is the application identifier for your project. It must be a domain name in reverse and must meet the requirements of all platforms that you want to target. If not specified, the application identifier is set to `org.nativescript.` <% if(isConsole) { %>For more information about the `` requirements, run `$ tns help create`<% } %><% if(isHtml) { %>For projects that target Android, you can use uppercase or lowercase letters, numbers, and underscores in the strings of the reversed domain name, separated by a dot. Strings must be separated by a dot and must start with a letter. For example: `com.nativescript.My_Andro1d_App` -For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens in the strings of the reversed domain name. Strings must be separated by a dot. For example: `com.nativescript.My-i0s-App`.<% } %> \ No newline at end of file +For projects that target iOS, you can use uppercase or lowercase letters, numbers, and hyphens in the strings of the reversed domain name. Strings must be separated by a dot. For example: `com.nativescript.My-i0s-App`.<% } %> + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[init](init.html) | Initializes a project for development. The command prompts you to provide your project configuration interactively and uses the information to create a new package.json file or update the existing one. +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/creation/init.md b/docs/man_pages/project/creation/init.md new file mode 100644 index 0000000000..1e05429b4e --- /dev/null +++ b/docs/man_pages/project/creation/init.md @@ -0,0 +1,21 @@ +init +========== + +Usage | Synopsis +---|--- +General | `$ tns init [--path ] [--force]` + +Initializes a project for development. The command prompts you to provide your project configuration interactively and uses the information to create a new `package.json` file or update the existing one. + +### Options +* `--path` - Specifies the directory where you want to initialize the project, if different from the current directory. The directory must be empty. +* `--force` - If set, applies the default project configuration and does not show the interactive prompt. The default project configuration targets the latest official runtimes and sets `org.nativescript.` for application identifier. + +<% if(isHtml) { %> +### Related Commands + +Command | Description +----------|---------- +[create](create.html) | Creates a new project for native development with NativeScript from the default template or from an existing NativeScript project. +[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory. +<% } %> \ No newline at end of file diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 22878c6913..945a8231a2 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -61,3 +61,9 @@ $injector.requireCommand("plugin|add", "./commands/plugin/add-plugin"); $injector.requireCommand("plugin|remove", "./commands/plugin/remove-plugin"); $injector.require("doctorService", "./services/doctor-service"); +$injector.requireCommand("install", "./commands/install"); + +$injector.require("initService", "./services/init-service"); +$injector.requireCommand("init", "./commands/init"); + +$injector.require("projectFilesManager", "./services/project-files-manager"); diff --git a/lib/commands/init.ts b/lib/commands/init.ts new file mode 100644 index 0000000000..52a2098f71 --- /dev/null +++ b/lib/commands/init.ts @@ -0,0 +1,16 @@ +/// +"use strict"; + +import Future = require("fibers/future"); + +export class InitCommand implements ICommand { + constructor(private $initService: IInitService) { } + + public allowedParameters: ICommandParameter[] = []; + public enableHooks = false; + + public execute(args: string[]): IFuture { + return this.$initService.initialize(); + } +} +$injector.registerCommand("init", InitCommand); \ No newline at end of file diff --git a/lib/commands/install.ts b/lib/commands/install.ts new file mode 100644 index 0000000000..e26e99aca9 --- /dev/null +++ b/lib/commands/install.ts @@ -0,0 +1,44 @@ +/// +"use strict"; + +export class InstallCommand implements ICommand { + private _projectData: any; + + constructor(private $platformsData: IPlatformsData, + private $platformService: IPlatformService, + private $projectData: IProjectData, + private $projectDataService: IProjectDataService, + private $pluginsService: IPluginsService, + private $logger: ILogger) { } + + public enableHooks = false; + + public allowedParameters: ICommandParameter[] = []; + + public execute(args: string[]): IFuture { + return (() => { + let error: string = ""; + + this.$pluginsService.ensureAllDependenciesAreInstalled().wait(); + + this.$projectDataService.initialize(this.$projectData.projectDir); + _.each(this.$platformsData.platformsNames, platform => { + let platformData = this.$platformsData.getPlatformData(platform); + let frameworkPackageData = this.$projectDataService.getValue(platformData.frameworkPackageName).wait(); + if(frameworkPackageData && frameworkPackageData.version) { + try { + this.$platformService.addPlatforms([`${platform}@${frameworkPackageData.version}`]).wait(); + } catch(err) { + error += err; + } + } + }); + + if(error) { + this.$logger.error(error); + } + + }).future()(); + } +} +$injector.registerCommand("install", InstallCommand); \ No newline at end of file diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index e96d511895..7f07f4644e 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -6,9 +6,7 @@ export class PrepareCommand implements ICommand { private $platformCommandParameter: ICommandParameter) { } execute(args: string[]): IFuture { - return (() => { - this.$platformService.preparePlatform(args[0]).wait(); - }).future()(); + return this.$platformService.preparePlatform(args[0]); } allowedParameters = [this.$platformCommandParameter]; diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 6b81087957..7bbbb1e596 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -28,7 +28,7 @@ export class RunAndroidCommand extends RunCommandBase implements ICommand { private $platformsData: IPlatformsData) { super($platformService); } - + public allowedParameters: ICommandParameter[] = []; public execute(args: string[]): IFuture { diff --git a/lib/common b/lib/common index 3bb71cefcf..76ba70be6f 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 3bb71cefcf2fd3818e90f79d99ef03c6a983e59e +Subproject commit 76ba70be6f7b385bc1a30ad25f94f85c104762d3 diff --git a/lib/config.ts b/lib/config.ts index e33d9173b6..beb73ba12d 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -57,5 +57,9 @@ export class StaticConfig extends staticConfigBaseLibPath.StaticConfigBase imple public get HTML_CLI_HELPERS_DIR(): string { return path.join(__dirname, "../docs/helpers"); } + + public get pathToPackageJson(): string { + return path.join(__dirname, "..", "package.json"); + } } $injector.register("staticConfig", StaticConfig); diff --git a/lib/constants.ts b/lib/constants.ts index a7c5e247e5..7d86bfbc49 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -7,6 +7,7 @@ export var NATIVESCRIPT_KEY_NAME = "nativescript"; export var NODE_MODULES_FOLDER_NAME = "node_modules"; export var PACKAGE_JSON_FILE_NAME = "package.json"; export var NODE_MODULE_CACHE_PATH_KEY_NAME = "node-modules-cache-path"; +export var DEFAULT_APP_IDENTIFIER_PREFIX = "org.nativescript"; export class ReleaseType { static MAJOR = "major"; diff --git a/lib/declarations.ts b/lib/declarations.ts index 031322e9c5..e07aa0d439 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -50,6 +50,8 @@ interface IOpener { interface IOptions extends ICommonOptions { frameworkPath: string; + frameworkName: string; + frameworkVersion: string; copyFrom: string; linkTo: string; release: boolean; @@ -64,3 +66,11 @@ interface IOptions extends ICommonOptions { keyStoreAliasPassword: string; sdk: string; } + +interface IProjectFilesManager { + processPlatformSpecificFiles(directoryPath: string, platform: string, excludedDirs?: string[]): IFuture; +} + +interface IInitService { + initialize(): IFuture; +} \ No newline at end of file diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index c45de5fc97..3ac9337349 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -27,6 +27,8 @@ interface IPlatformProjectService { interpolateData(projectRoot: string): IFuture; afterCreateProject(projectRoot: string): IFuture; buildProject(projectRoot: string): IFuture; + prepareProject(): IFuture; + prepareAppResources(appResourcesDirectoryPath: string): IFuture; isPlatformPrepared(projectRoot: string): IFuture; addLibrary(platformData: IPlatformData, libraryPath: string): IFuture; canUpdatePlatform(currentVersion: string, newVersion: string): IFuture; diff --git a/lib/options.ts b/lib/options.ts index 08961c412e..bd2e02c256 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -13,6 +13,8 @@ export class Options extends commonOptionsLibPath.OptionsBase { $hostInfo: IHostInfo) { super({ frameworkPath: { type: OptionType.String }, + frameworkName: { type: OptionType.String }, + frameworkVersion: { type: OptionType.String }, copyFrom: { type: OptionType.String }, linkTo: { type: OptionType.String }, release: { type: OptionType.Boolean }, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 9dfb88947e..1561015c85 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -51,7 +51,7 @@ class AndroidProjectService implements IPlatformProjectService { frameworkFilesExtensions: [".jar", ".dat", ".so"], configurationFileName: "AndroidManifest.xml", configurationFilePath: path.join(this.$projectData.platformsDir, "android", "AndroidManifest.xml"), - mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }] + mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }, { "application": "manifest", "attrname": "*" }] }; } @@ -181,6 +181,16 @@ class AndroidProjectService implements IPlatformProjectService { public isPlatformPrepared(projectRoot: string): IFuture { return this.$fs.exists(path.join(projectRoot, "assets", constants.APP_FOLDER_NAME)); } + + public prepareAppResources(appResourcesDirectoryPath: string): IFuture { + return (() => { + let resourcesDirPath = path.join(appResourcesDirectoryPath, this.platformData.normalizedPlatformName); + let resourcesDirs = this.$fs.readDirectory(resourcesDirPath).wait(); + _.each(resourcesDirs, resourceDir => { + this.$fs.deleteDirectory(path.join(this.platformData.appResourcesDestinationDirectoryPath, resourceDir)).wait(); + }); + }).future()(); + } private parseProjectProperties(projDir: string, destDir: string): void { let projProp = path.join(projDir, "project.properties"); @@ -273,6 +283,10 @@ class AndroidProjectService implements IPlatformProjectService { public getFrameworkFilesExtensions(): string[] { return [".jar", ".dat"]; } + + public prepareProject(): IFuture { + return (() => { }).future()(); + } private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): IFuture { return (() => { diff --git a/lib/services/init-service.ts b/lib/services/init-service.ts new file mode 100644 index 0000000000..e30f5963cd --- /dev/null +++ b/lib/services/init-service.ts @@ -0,0 +1,120 @@ +/// +"use strict"; + +import constants = require("./../constants"); +import helpers = require("./../common/helpers"); +import path = require("path"); +import semver = require("semver"); + +export class InitService implements IInitService { + private static MIN_SUPPORTED_FRAMEWORK_VERSIONS: IStringDictionary = { + "tns-ios": "1.1.0", + "tns-android": "1.1.0" + }; + + private _projectFilePath: string; + + constructor(private $fs: IFileSystem, + private $errors: IErrors, + private $logger: ILogger, + private $options: IOptions, + private $injector: IInjector, + private $staticConfig: IStaticConfig, + private $projectHelper: IProjectHelper, + private $prompter: IPrompter, + private $npm: INodePackageManager, + private $npmInstallationManager: INpmInstallationManager) { } + + public initialize(): IFuture { + return (() => { + let projectData: any = { }; + + if(this.$fs.exists(this.projectFilePath).wait()) { + projectData = this.$fs.readJson(this.projectFilePath).wait(); + } + + let projectDataBackup = _.extend({}, projectData); + + if(!projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]) { + projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE] = { }; + this.$fs.writeJson(this.projectFilePath, projectData).wait(); // We need to create package.json file here in order to prevent "No project found at or above and neither was a --path specified." when resolving platformsData + } + + try { + + projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]["id"] = this.getProjectId().wait(); + + if(this.$options.frameworkName && this.$options.frameworkVersion) { + projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] = this.buildVersionData(this.$options.frameworkVersion); + } else { + let $platformsData = this.$injector.resolve("platformsData"); + _.each($platformsData.platformsNames, platform => { + let platformData: IPlatformData = $platformsData.getPlatformData(platform); + if(!platformData.targetedOS || (platformData.targetedOS && _.contains(platformData.targetedOS, process.platform))) { + projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] = this.getVersionData(platformData.frameworkPackageName).wait(); + } + }); + } + + this.$fs.writeJson(this.projectFilePath, projectData).wait(); + } catch(err) { + this.$fs.writeJson(this.projectFilePath, projectDataBackup).wait(); + throw err; + } + + this.$logger.out("Project successfully initialized."); + }).future()(); + } + + private get projectFilePath(): string { + if(!this._projectFilePath) { + let projectDir = path.resolve(this.$options.path || "."); + this._projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); + } + + return this._projectFilePath; + } + + private getProjectId(): IFuture { + return (() => { + if(this.$options.appid) { + return this.$options.appid; + } + + let defaultAppId = this.$projectHelper.generateDefaultAppId(path.basename(path.dirname(this.projectFilePath)), constants.DEFAULT_APP_IDENTIFIER_PREFIX); + if(this.useDefaultValue) { + return defaultAppId; + } + + return this.$prompter.getString("Id:", () => defaultAppId).wait(); + }).future()(); + } + + private getVersionData(packageName: string): IFuture { + return (() => { + let latestVersion = this.$npmInstallationManager.getLatestVersion(packageName).wait(); + if(this.useDefaultValue) { + return this.buildVersionData(latestVersion); + } + + let data = this.$npm.view(packageName, "versions").wait(); + let versions = _.filter(data[latestVersion].versions, (version: string) => semver.gte(version, InitService.MIN_SUPPORTED_FRAMEWORK_VERSIONS[packageName])); + if(versions.length === 1) { + this.$logger.info(`Only ${versions[0]} version is available for ${packageName} framework.`); + return this.buildVersionData(versions[0]); + } + let sortedVersions = versions.sort(helpers.versionCompare).reverse(); + let version = this.$prompter.promptForChoice(`${packageName} version:`, sortedVersions).wait(); + return this.buildVersionData(version); + }).future()(); + } + + private buildVersionData(version: string): IStringDictionary { + return { "version": version }; + } + + private get useDefaultValue(): boolean { + return !helpers.isInteractive() || this.$options.force; + } +} +$injector.register("initService", InitService); \ No newline at end of file diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index d0f1a5d9b5..810244d4c0 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -182,13 +182,11 @@ class IOSProjectService implements IPlatformProjectService { this.$fs.ensureDirectoryExists(targetPath).wait(); shell.cp("-R", libraryPath, targetPath); - var pbxProjPath = path.join(platformData.projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj"); - var project = new xcode.project(pbxProjPath); - project.parseSync(); + let project = this.createPbxProj(); project.addFramework(path.join(targetPath, frameworkName + ".framework"), { customFramework: true, embed: true }); project.updateBuildProperty("IPHONEOS_DEPLOYMENT_TARGET", "8.0"); - this.$fs.writeFile(pbxProjPath, project.writeSync()).wait(); + this.savePbxProj(project).wait(); this.$logger.info("The iOS Deployment Target is now 8.0 in order to support Cocoa Touch Frameworks."); }).future()(); } @@ -221,11 +219,67 @@ class IOSProjectService implements IPlatformProjectService { shell.cp("-R", path.join(cachedPackagePath, "*"), path.join(this.platformData.projectRoot, util.format("%s.xcodeproj", this.$projectData.projectName))); this.$logger.info("Copied from %s at %s.", cachedPackagePath, this.platformData.projectRoot); - var pbxprojFilePath = path.join(this.platformData.projectRoot, this.$projectData.projectName + IOSProjectService.XCODE_PROJECT_EXT_NAME, "project.pbxproj"); this.replaceFileContent(pbxprojFilePath).wait(); }).future()(); } + + public prepareProject(): IFuture { + return (() => { + let project = this.createPbxProj(); + let resources = project.pbxGroupByName("Resources"); + + if(resources) { + let references = project.pbxFileReferenceSection(); + + let xcodeProjectImages = _.map(resources.children, resource => this.replace(references[resource.value].name)); + this.$logger.trace("Images from Xcode project"); + this.$logger.trace(xcodeProjectImages); + + let appResourcesImages = this.$fs.readDirectory(this.platformData.appResourcesDestinationDirectoryPath).wait(); + this.$logger.trace("Current images from App_Resources"); + this.$logger.trace(appResourcesImages); + + let imagesToAdd = _.difference(appResourcesImages, xcodeProjectImages); + this.$logger.trace(`New images to add into xcode project: ${imagesToAdd.join(", ")}`); + _.each(imagesToAdd, image => project.addResourceFile(path.relative(this.platformData.projectRoot, path.join( this.platformData.appResourcesDestinationDirectoryPath, image)))); + + let imagesToRemove = _.difference(xcodeProjectImages, appResourcesImages); + this.$logger.trace(`Images to remove from xcode project: ${imagesToRemove.join(", ")}`); + _.each(imagesToRemove, image => project.removeResourceFile(path.join(this.platformData.appResourcesDestinationDirectoryPath, image))); + + this.savePbxProj(project).wait(); + } + + }).future()(); + } + + public prepareAppResources(appResourcesDirectoryPath: string): IFuture { + return this.$fs.deleteDirectory(this.platformData.appResourcesDestinationDirectoryPath); + } + + private replace(name: string): string { + if(_.startsWith(name, '"')) { + name = name.substr(1, name.length-2); + } + + return name.replace(/\\\"/g, "\""); + } + + private get pbxProjPath(): string { + return path.join(this.platformData.projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj"); + } + + private createPbxProj(): any { + let project = new xcode.project(this.pbxProjPath); + project.parseSync(); + + return project; + } + + private savePbxProj(project: any): IFuture { + return this.$fs.writeFile(this.pbxProjPath, project.writeSync()); + } private buildPathToXcodeProjectFile(version: string): string { return path.join(this.$npmInstallationManager.getCachedPackagePath(this.platformData.frameworkPackageName, version), constants.PROJECT_FRAMEWORK_FOLDER_NAME, util.format("%s.xcodeproj", IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER), "project.pbxproj"); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 9729c23b89..5d94d8d11d 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -23,7 +23,8 @@ export class PlatformService implements IPlatformService { private $commandsService: ICommandsService, private $options: IOptions, private $broccoliBuilder: IBroccoliBuilder, - private $pluginsService: IPluginsService) { } + private $pluginsService: IPluginsService, + private $projectFilesManager: IProjectFilesManager) { } public addPlatforms(platforms: string[]): IFuture { return (() => { @@ -145,44 +146,48 @@ export class PlatformService implements IPlatformService { public preparePlatform(platform: string): IFuture { return (() => { + this.validatePlatform(platform); + platform = platform.toLowerCase(); var platformData = this.$platformsData.getPlatformData(platform); + let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + let lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath).wait() ? + this.$fs.getFsStats(appDestinationDirectoryPath).wait().mtime : null; // Copy app folder to native project + this.$fs.ensureDirectoryExists(appDestinationDirectoryPath).wait(); var appSourceDirectoryPath = path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME); - + // Delete the destination app in order to prevent EEXIST errors when symlinks are used. - this.$fs.deleteDirectory(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)).wait(); - shell.cp("-R", appSourceDirectoryPath, platformData.appDestinationDirectoryPath); + let contents = this.$fs.readDirectory(appDestinationDirectoryPath).wait(); + + _(contents) + .filter(directoryName => directoryName !== "tns_modules") + .each(directoryName => this.$fs.deleteDirectory(path.join(appDestinationDirectoryPath, directoryName)).wait()) + .value(); + shell.cp("-Rf", appSourceDirectoryPath, platformData.appDestinationDirectoryPath); // Copy App_Resources to project root folder this.$fs.ensureDirectoryExists(platformData.appResourcesDestinationDirectoryPath).wait(); // Should be deleted - var appResourcesDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); + var appResourcesDirectoryPath = path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME); if (this.$fs.exists(appResourcesDirectoryPath).wait()) { - shell.cp("-Rf", path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName, "*"), platformData.appResourcesDestinationDirectoryPath); + platformData.platformProjectService.prepareAppResources(appResourcesDirectoryPath).wait(); + shell.cp("-R", path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName, "*"), platformData.appResourcesDestinationDirectoryPath); this.$fs.deleteDirectory(appResourcesDirectoryPath).wait(); } + + platformData.platformProjectService.prepareProject().wait(); // Process platform specific files - var contents = this.$fs.readDirectory(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)).wait(); - var files: string[] = []; - - _.each(contents, d => { - let filePath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, d); - let fsStat = this.$fs.getFsStats(filePath).wait(); - if(fsStat.isDirectory() && d !== constants.APP_RESOURCES_FOLDER_NAME) { - this.processPlatformSpecificFiles(platform, this.$fs.enumerateFilesInDirectorySync(filePath)).wait(); - } else if(fsStat.isFile()) { - files.push(filePath); - } - }); - this.processPlatformSpecificFiles(platform, files).wait(); + let directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + let excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; + this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, excludedDirs).wait(); // Process node_modules folder this.$pluginsService.ensureAllDependenciesAreInstalled().wait(); var tnsModulesDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, PlatformService.TNS_MODULES_FOLDER_NAME); - this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, this.$projectData.projectDir).wait(); + this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, this.$projectData.projectDir, platform, lastModifiedTime).wait(); this.$logger.out("Project successfully prepared"); }).future()(); @@ -342,33 +347,6 @@ export class PlatformService implements IPlatformService { return platformData.platformProjectService.isPlatformPrepared(platformData.projectRoot); } - private static parsePlatformSpecificFileName(fileName: string, platforms: string[]): any { - var regex = util.format("^(.+?)\\.(%s)(\\..+?)$", platforms.join("|")); - var parsed = fileName.match(new RegExp(regex, "i")); - if (parsed) { - return { - platform: parsed[2], - onDeviceName: parsed[1] + parsed[3] - }; - } - return undefined; - } - - private processPlatformSpecificFiles( platform: string, files: string[]): IFuture { - // Renames the files that have `platform` as substring and removes the files from other platform - return (() => { - _.each(files, fileName => { - var platformInfo = PlatformService.parsePlatformSpecificFileName(path.basename(fileName), this.$platformsData.platformsNames); - var shouldExcludeFile = platformInfo && platformInfo.platform !== platform; - if (shouldExcludeFile) { - this.$fs.deleteFile(fileName).wait(); - } else if (platformInfo && platformInfo.onDeviceName) { - this.$fs.rename(fileName, path.join(path.dirname(fileName), platformInfo.onDeviceName)).wait(); - } - }); - }).future()(); - } - private getApplicationPackages(buildOutputPath: string, validPackageNames: string[]): IFuture { return (() => { // Get latest package that is produced from build diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index de68eebb41..b5e5b10559 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -24,7 +24,7 @@ export class PluginsService implements IPluginsService { private $options: IOptions, private $logger: ILogger, private $errors: IErrors, - private $injector: IInjector) { } + private $projectFilesManager: IProjectFilesManager) { } public add(plugin: string): IFuture { return (() => { @@ -101,7 +101,9 @@ export class PluginsService implements IPluginsService { if(this.$fs.exists(pluginPlatformsFolderPath).wait()) { shelljs.rm("-rf", pluginPlatformsFolderPath); - } + } + + this.$projectFilesManager.processPlatformSpecificFiles(pluginDestinationPath, platform).wait(); // TODO: Add libraries diff --git a/lib/services/project-files-manager.ts b/lib/services/project-files-manager.ts new file mode 100644 index 0000000000..40be8f4336 --- /dev/null +++ b/lib/services/project-files-manager.ts @@ -0,0 +1,56 @@ +/// +"use strict"; +import path = require("path"); +import util = require("util"); + +export class ProjectFilesManager implements IProjectFilesManager { + constructor(private $fs: IFileSystem, + private $platformsData: IPlatformsData) { } + + public processPlatformSpecificFiles(directoryPath: string, platform: string, excludedDirs?: string[]): IFuture { + return (() => { + var contents = this.$fs.readDirectory(directoryPath).wait(); + var files: string[] = []; + + _.each(contents, fileName => { + let filePath = path.join(directoryPath, fileName); + let fsStat = this.$fs.getFsStats(filePath).wait(); + if(fsStat.isDirectory() && !_.contains(excludedDirs, fileName)) { + this.processPlatformSpecificFilesCore(platform, this.$fs.enumerateFilesInDirectorySync(filePath)).wait(); + } else if(fsStat.isFile()) { + files.push(filePath); + } + }); + this.processPlatformSpecificFilesCore(platform, files).wait(); + + }).future()(); + } + + private processPlatformSpecificFilesCore(platform: string, files: string[]): IFuture { + // Renames the files that have `platform` as substring and removes the files from other platform + return (() => { + _.each(files, fileName => { + var platformInfo = ProjectFilesManager.parsePlatformSpecificFileName(path.basename(fileName), this.$platformsData.platformsNames); + var shouldExcludeFile = platformInfo && platformInfo.platform !== platform; + if (shouldExcludeFile) { + this.$fs.deleteFile(fileName).wait(); + } else if (platformInfo && platformInfo.onDeviceName) { + this.$fs.rename(fileName, path.join(path.dirname(fileName), platformInfo.onDeviceName)).wait(); + } + }); + }).future()(); + } + + private static parsePlatformSpecificFileName(fileName: string, platforms: string[]): any { + var regex = util.format("^(.+?)\\.(%s)(\\..+?)$", platforms.join("|")); + var parsed = fileName.match(new RegExp(regex, "i")); + if (parsed) { + return { + platform: parsed[2], + onDeviceName: parsed[1] + parsed[3] + }; + } + return undefined; + } +} +$injector.register("projectFilesManager", ProjectFilesManager); diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index a9f15591c5..16aa0aeda0 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -9,7 +9,6 @@ import shell = require("shelljs"); import util = require("util"); export class ProjectService implements IProjectService { - private static DEFAULT_APP_IDENTIFIER_PREFIX = "org.nativescript"; constructor(private $errors: IErrors, private $fs: IFileSystem, @@ -27,7 +26,7 @@ export class ProjectService implements IProjectService { } this.$projectNameValidator.validate(projectName); - var projectId = this.$options.appid || this.$projectHelper.generateDefaultAppId(projectName, ProjectService.DEFAULT_APP_IDENTIFIER_PREFIX); + var projectId = this.$options.appid || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX); var projectDir = path.join(path.resolve(this.$options.path || "."), projectName); this.$fs.createDirectory(projectDir).wait(); diff --git a/lib/tools/broccoli/broccoli.d.ts b/lib/tools/broccoli/broccoli.d.ts index 4ca05877c3..7d4ba88820 100644 --- a/lib/tools/broccoli/broccoli.d.ts +++ b/lib/tools/broccoli/broccoli.d.ts @@ -152,7 +152,7 @@ interface BroccoliNode { } interface IBroccoliBuilder { - prepareNodeModules(outputPath: string, projectDir: string): IFuture; + prepareNodeModules(outputPath: string, projectDir: string, platform: string, lastModifiedTime?: Date): IFuture; } interface IDiffResult { @@ -162,6 +162,7 @@ interface IDiffResult { interface IBroccoliPlugin { rebuild(diff: IDiffResult): any; + rebuildChangedDirectories?(changedDirectories: string[], platform: string): void; cleanup? () : void; } diff --git a/lib/tools/broccoli/builder.ts b/lib/tools/broccoli/builder.ts index 070b5259b8..9d8fba9437 100644 --- a/lib/tools/broccoli/builder.ts +++ b/lib/tools/broccoli/builder.ts @@ -5,29 +5,82 @@ let broccoli = require('broccoli'); let path = require('path'); import Future = require("fibers/future"); import {TreeDiffer} from './tree-differ'; -import destCopy = require('./node-modules-dest-copy'); +import destCopyLib = require('./node-modules-dest-copy'); + +var gulp = require("gulp"); +var vinylFilterSince = require('vinyl-filter-since') +var through = require("through2"); export class Builder implements IBroccoliBuilder { + private nodeModules: any = {}; + constructor(private $fs: IFileSystem, private $nodeModulesTree: INodeModulesTree, private $projectDataService: IProjectDataService, + private $injector: IInjector, private $logger: ILogger) { } - public prepareNodeModules(absoluteOutputPath: string, projectDir: string): IFuture { + public prepareNodeModules(absoluteOutputPath: string, projectDir: string, platform: string, lastModifiedTime?: Date): IFuture { return (() => { - // TODO: figure out a better way for doing this - this.$projectDataService.initialize(projectDir); - let cachedNodeModulesPath = this.$projectDataService.getValue("node-modules-cache-path").wait(); - if (cachedNodeModulesPath && this.$fs.exists(cachedNodeModulesPath).wait()) { - let diffTree = new TreeDiffer(cachedNodeModulesPath); - let diffTreeResult = diffTree.diffTree(path.join(projectDir, absoluteOutputPath, "node_modules")); - - if(diffTreeResult.changedDirectories.length > 0 || diffTreeResult.removedDirectories.length > 0) { - this.rebuildNodeModulesTree(absoluteOutputPath, projectDir).wait(); - } - } else { - this.rebuildNodeModulesTree(absoluteOutputPath, projectDir).wait(); + let isNodeModulesModified = false; + let nodeModulesPath = path.join(projectDir, "node_modules"); + + if(lastModifiedTime) { + let pipeline = gulp.src(path.join(projectDir, "node_modules/**")) + .pipe(vinylFilterSince(lastModifiedTime)) + .pipe(through.obj( (chunk: any, enc: any, cb: Function) => { + if(chunk.path === nodeModulesPath) { + isNodeModulesModified = true; + } + + if(!isNodeModulesModified) { + let rootModuleName = chunk.path.split(nodeModulesPath)[1].split(path.sep)[1]; + let rootModuleFullPath = path.join(nodeModulesPath, rootModuleName); + this.nodeModules[rootModuleFullPath] = rootModuleFullPath; + } + + cb(null); + })) + .pipe(gulp.dest(absoluteOutputPath)); + + let future = new Future(); + + pipeline.on('end', (err: any, data: any) => { + if(err) { + future.throw(err); + } else { + future.return(); + } + }); + + future.wait(); + } + + if(isNodeModulesModified && this.$fs.exists(absoluteOutputPath).wait()) { + let currentPreparedTnsModules = this.$fs.readDirectory(absoluteOutputPath).wait(); + let tnsModulesInApp = this.$fs.readDirectory(path.join(projectDir, "app", "tns_modules")).wait(); + let modulesToDelete = _.difference(currentPreparedTnsModules, tnsModulesInApp); + _.each(modulesToDelete, moduleName => this.$fs.deleteDirectory(path.join(absoluteOutputPath, moduleName)).wait()) + } + + if(!lastModifiedTime || isNodeModulesModified) { + let nodeModulesDirectories = this.$fs.readDirectory(nodeModulesPath).wait(); + _.each(nodeModulesDirectories, nodeModuleDirectoryName => { + let nodeModuleFullPath = path.join(nodeModulesPath, nodeModuleDirectoryName); + this.nodeModules[nodeModuleFullPath] = nodeModuleFullPath; + }); } + + let destCopy = this.$injector.resolve(destCopyLib.DestCopy, { + inputPath: projectDir, + cachePath: "", + outputRoot: absoluteOutputPath, + projectDir: projectDir, + platform: platform + }); + + destCopy.rebuildChangedDirectories(_.keys(this.nodeModules)); + }).future()(); } diff --git a/lib/tools/broccoli/node-modules-dest-copy.ts b/lib/tools/broccoli/node-modules-dest-copy.ts index 61b0f3e801..45941440ec 100644 --- a/lib/tools/broccoli/node-modules-dest-copy.ts +++ b/lib/tools/broccoli/node-modules-dest-copy.ts @@ -14,57 +14,73 @@ import constants = require("./../../constants"); * and tees a copy to the given path outside the tmp dir. */ export class DestCopy implements IBroccoliPlugin { - constructor(private inputPath: string, private cachePath: string, private outputRoot: string, private projectDir: string) {} - - public rebuild(treeDiff: IDiffResult): void { - let dependencies = this.getDependencies(); - let devDependencies = this.getDevDependencies(this.projectDir); - - treeDiff.changedDirectories.forEach(changedDirectory => { - let changedDirectoryAbsolutePath = path.join(this.inputPath, constants.NODE_MODULES_FOLDER_NAME, changedDirectory); - let packageJsonFiles = [path.join(changedDirectoryAbsolutePath, "package.json")]; - let nodeModulesFolderPath = path.join(changedDirectoryAbsolutePath, "node_modules"); - packageJsonFiles = packageJsonFiles.concat(this.enumeratePackageJsonFilesSync(nodeModulesFolderPath)); - - _.each(packageJsonFiles, packageJsonFilePath => { - let fileContent = require(packageJsonFilePath); - let isPlugin = fileContent.nativescript; + private dependencies: IDictionary = null; + private devDependencies: IDictionary = null; - if(!devDependencies[fileContent.name]) { // Don't flatten dev dependencies - - let currentDependency = { - name: fileContent.name, - version: fileContent.version, - directory: path.dirname(packageJsonFilePath), - isPlugin: isPlugin - }; + constructor(private inputPath: string, + private cachePath: string, + private outputRoot: string, + private projectDir: string, + private platform: string, + private $fs: IFileSystem, + private $projectFilesManager: IProjectFilesManager) { + this.dependencies = Object.create(null); + this.devDependencies = this.getDevDependencies(projectDir); + } + + public rebuildChangedDirectories(changedDirectories: string[], platform: string): void { + _.each(changedDirectories, changedDirectoryAbsolutePath => { + if(!this.devDependencies[path.basename(changedDirectoryAbsolutePath)]) { + let pathToPackageJson = path.join(changedDirectoryAbsolutePath, constants.PACKAGE_JSON_FILE_NAME); + let packageJsonFiles = fs.existsSync(pathToPackageJson) ? [pathToPackageJson] : []; + let nodeModulesFolderPath = path.join(changedDirectoryAbsolutePath, constants.NODE_MODULES_FOLDER_NAME); + packageJsonFiles = packageJsonFiles.concat(this.enumeratePackageJsonFilesSync(nodeModulesFolderPath)); - let addedDependency = dependencies[currentDependency.name]; - if (addedDependency) { - if (semver.gt(currentDependency.version, addedDependency.version)) { - let currentDependencyMajorVersion = semver.major(currentDependency.version); - let addedDependencyMajorVersion = semver.major(addedDependency.version); + _.each(packageJsonFiles, packageJsonFilePath => { + let fileContent = require(packageJsonFilePath); - let message = `The depedency located at ${addedDependency.directory} with version ${addedDependency.version} will be replaced with dependency located at ${currentDependency.directory} with version ${currentDependency.version}`; - let logger = $injector.resolve("$logger"); - currentDependencyMajorVersion === addedDependencyMajorVersion ? logger.out(message) : logger.warn(message); - - dependencies[currentDependency.name] = currentDependency; + if(!this.devDependencies[fileContent.name]) { // Don't flatten dev dependencies + let isPlugin = fileContent.nativescript; + + let currentDependency = { + name: fileContent.name, + version: fileContent.version, + directory: path.dirname(packageJsonFilePath), + isPlugin: isPlugin + }; + + let addedDependency = this.dependencies[currentDependency.name]; + if (addedDependency) { + if (semver.gt(currentDependency.version, addedDependency.version)) { + let currentDependencyMajorVersion = semver.major(currentDependency.version); + let addedDependencyMajorVersion = semver.major(addedDependency.version); + + let message = `The depedency located at ${addedDependency.directory} with version ${addedDependency.version} will be replaced with dependency located at ${currentDependency.directory} with version ${currentDependency.version}`; + let logger = $injector.resolve("$logger"); + currentDependencyMajorVersion === addedDependencyMajorVersion ? logger.out(message) : logger.warn(message); + + this.dependencies[currentDependency.name] = currentDependency; + } + } else { + this.dependencies[currentDependency.name] = currentDependency; } - } else { - dependencies[currentDependency.name] = currentDependency; } - } - }); + }); + } }); - _.each(dependencies, dependency => { - shelljs.cp("-R", dependency.directory, this.outputRoot); + _.each(this.dependencies, dependency => { + shelljs.cp("-Rf", dependency.directory, this.outputRoot); shelljs.rm("-rf", path.join(this.outputRoot, dependency.name, "node_modules")); if(dependency.isPlugin) { + this.$projectFilesManager.processPlatformSpecificFiles(path.join(this.outputRoot, dependency.name), platform).wait(); shelljs.rm("-rf", path.join(this.outputRoot, dependency.name, "platforms")); } }); + } + + public rebuild(treeDiff: IDiffResult): void { + this.rebuildChangedDirectories(treeDiff.changedDirectories, ""); // Cache input tree let projectFilePath = path.join(this.projectDir, constants.PACKAGE_JSON_FILE_NAME); @@ -73,22 +89,6 @@ export class DestCopy implements IBroccoliPlugin { fs.writeFileSync(projectFilePath, JSON.stringify(projectFileContent, null, "\t"), { encoding: "utf8" }); } - private getDependencies(): IDictionary { - let result = Object.create(null); - if(fs.existsSync(this.outputRoot)) { - let dirs = fs.readdirSync(this.outputRoot); - _.each(dirs, dir => { - let filePath = path.join(dir, constants.PACKAGE_JSON_FILE_NAME); - if(fs.existsSync(filePath)) { - let fileContent = require(filePath); - result[fileContent.name] = fileContent; - } - }); - } - - return result; - } - private getDevDependencies(projectDir: string): IDictionary { let projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); let projectFileContent = require(projectFilePath); @@ -100,9 +100,12 @@ export class DestCopy implements IBroccoliPlugin { if(fs.existsSync(nodeModulesDirectoryPath)) { let contents = fs.readdirSync(nodeModulesDirectoryPath); for (let i = 0; i < contents.length; ++i) { - foundFiles.push(path.join(nodeModulesDirectoryPath, contents[i], "package.json")); + let packageJsonFilePath = path.join(nodeModulesDirectoryPath, contents[i], constants.PACKAGE_JSON_FILE_NAME); + if (fs.existsSync(packageJsonFilePath)) { + foundFiles.push(packageJsonFilePath); + } - var directoryPath = path.join(nodeModulesDirectoryPath, contents[i], "node_modules"); + var directoryPath = path.join(nodeModulesDirectoryPath, contents[i], constants.NODE_MODULES_FOLDER_NAME); if (fs.existsSync(directoryPath)) { this.enumeratePackageJsonFilesSync(directoryPath, foundFiles); } diff --git a/package.json b/package.json index 86069efb64..46a84bde23 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "preferGlobal": true, - "version": "1.1.1", + "version": "1.1.2", "author": "Telerik ", "description": "Command-line interface for building NativeScript projects", "bin": { @@ -37,6 +37,7 @@ "fibers": "https://github.com/icenium/node-fibers/tarball/v1.0.5.1", "filesize": "2.0.3", "gaze": "0.5.1", + "gulp": "3.9.0", "iconv-lite": "0.4.4", "inquirer": "0.8.2", "ios-sim-portable": "1.0.8", @@ -65,10 +66,12 @@ "shelljs": "0.3.0", "tabtab": "https://github.com/Icenium/node-tabtab/tarball/master", "temp": "0.8.1", + "through2": "2.0.0", "utf-8-validate": "https://github.com/telerik/utf-8-validate/tarball/master", + "vinyl-filter-since": "2.0.0", "winreg": "0.0.12", "ws": "0.7.1", - "xcode": "https://github.com/NativeScript/node-xcode/archive/NativeScript-0.9.tar.gz", + "xcode": "https://github.com/NativeScript/node-xcode/archive/NativeScript-1.1.tar.gz", "xmldom": "0.1.19", "xmlhttprequest": "https://github.com/telerik/node-XMLHttpRequest/tarball/master", "xmlmerge-js": "0.2.4", diff --git a/test/npm-support.ts b/test/npm-support.ts index d7a557f79e..fff3f12ca7 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -19,6 +19,8 @@ import BroccoliBuilderLib = require("../lib/tools/broccoli/builder"); import NodeModulesTreeLib = require("../lib/tools/broccoli/trees/node-modules-tree"); import PluginsServiceLib = require("../lib/services/plugins-service"); import ChildProcessLib = require("../lib/common/child-process"); +import ProjectFilesManagerLib = require("../lib/services/project-files-manager"); +import Future = require("fibers/future"); import path = require("path"); import temp = require("temp"); @@ -53,6 +55,7 @@ function createTestInjector(): IInjector { testInjector.register("pluginsService", PluginsServiceLib.PluginsService); testInjector.register("npm", NpmLib.NodePackageManager); testInjector.register("childProcess", ChildProcessLib.ChildProcess); + testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); testInjector.register("commandsServiceProvider", { registerDynamicSubCommands: () => {} }); @@ -80,20 +83,16 @@ function createProject(testInjector: IInjector, dependencies?: any): string { } }; packageJsonData["dependencies"] = dependencies; + packageJsonData["devDependencies"] = {}; testInjector.resolve("fs").writeJson(path.join(tempFolder, "package.json"), packageJsonData).wait(); return tempFolder; } -describe("Npm support tests", () => { - it("Ensures that the installed dependencies are prepared correctly", () => { - let dependencies = { - "bplist": "0.0.4", - "lodash": "3.9.3" - }; - +function setupProject(): IFuture { + return (() => { let testInjector = createTestInjector(); - let projectFolder = createProject(testInjector, dependencies); + let projectFolder = createProject(testInjector); let fs = testInjector.resolve("fs"); @@ -109,7 +108,6 @@ describe("Npm support tests", () => { let androidFolderPath = path.join(projectFolder, "platforms", "android"); fs.ensureDirectoryExists(androidFolderPath).wait(); - // Mock platform data let appDestinationFolderPath = path.join(androidFolderPath, "assets"); let platformsData = testInjector.resolve("platformsData"); @@ -118,12 +116,58 @@ describe("Npm support tests", () => { appDestinationDirectoryPath: appDestinationFolderPath, appResourcesDestinationDirectoryPath: path.join(appDestinationFolderPath, "app", "App_Resources"), frameworkPackageName: "tns-android", - normalizedPlatformName: "Android" + normalizedPlatformName: "Android", + platformProjectService: { + prepareProject: () => Future.fromResult(), + prepareAppResources: () => Future.fromResult() + } } }; - let platformService = testInjector.resolve("platformService"); - platformService.preparePlatform("android").wait(); + return { + testInjector: testInjector, + projectFolder: projectFolder, + appDestinationFolderPath: appDestinationFolderPath, + }; + }).future()(); +} + +function addDependencies(testInjector: IInjector, projectFolder: string, dependencies: any, devDependencies?: any): IFuture { + return (() => { + let fs = testInjector.resolve("fs"); + let packageJsonPath = path.join(projectFolder, "package.json"); + let packageJsonData = fs.readJson(packageJsonPath).wait(); + + let currentDependencies = packageJsonData.dependencies; + _.extend(currentDependencies, dependencies); + + if(devDependencies) { + let currentDevDependencies = packageJsonData.devDependencies; + _.extend(currentDevDependencies, devDependencies); + } + fs.writeJson(packageJsonPath, packageJsonData).wait(); + }).future()(); +} + +function preparePlatform(testInjector: IInjector): IFuture { + let platformService = testInjector.resolve("platformService"); + return platformService.preparePlatform("android"); +} + +describe("Npm support tests", () => { + let testInjector: IInjector, projectFolder: string, appDestinationFolderPath: string; + before(() => { + let projectSetup = setupProject().wait(); + testInjector = projectSetup.testInjector; + projectFolder = projectSetup.projectFolder; + appDestinationFolderPath = projectSetup.appDestinationFolderPath; + }); + it("Ensures that the installed dependencies are prepared correctly", () => { + // Setup + addDependencies(testInjector, projectFolder, {"bplist": "0.0.4"}).wait(); + + // Act + preparePlatform(testInjector).wait(); // Assert let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); @@ -132,9 +176,63 @@ describe("Npm support tests", () => { let bplistCreatorFolderPath = path.join(tnsModulesFolderPath, "bplist-creator"); let bplistParserFolderPath = path.join(tnsModulesFolderPath, "bplist-parser"); + let fs = testInjector.resolve("fs"); assert.isTrue(fs.exists(lodashFolderPath).wait()); assert.isTrue(fs.exists(bplistFolderPath).wait()); assert.isTrue(fs.exists(bplistCreatorFolderPath).wait()); assert.isTrue(fs.exists(bplistParserFolderPath).wait()); }); +}); + +describe("Flatten npm modules tests", () => { + it("Doesn't handle the dependencies of devDependencies", () => { + let projectSetup = setupProject().wait(); + let testInjector = projectSetup.testInjector; + let projectFolder = projectSetup.projectFolder; + let appDestinationFolderPath = projectSetup.appDestinationFolderPath; + + let devDependencies = { + "gulp": "3.9.0", + "gulp-jscs": "1.6.0", + "gulp-jshint": "1.11.0" + }; + + addDependencies(testInjector, projectFolder, {}, devDependencies).wait(); + + preparePlatform(testInjector).wait(); + + // Assert + let fs = testInjector.resolve("fs"); + let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + + let lodashFolderPath = path.join(tnsModulesFolderPath, "lodash"); + assert.isTrue(fs.exists(lodashFolderPath).wait()); + + let gulpFolderPath = path.join(tnsModulesFolderPath, "gulp"); + assert.isFalse(fs.exists(gulpFolderPath).wait()); + + let gulpJscsFolderPath = path.join(tnsModulesFolderPath, "gulp-jscs"); + assert.isFalse(fs.exists(gulpJscsFolderPath).wait()); + + let gulpJshint = path.join(tnsModulesFolderPath, "gulp-jshint"); + assert.isFalse(fs.exists(gulpJshint).wait()); + + // Get all gulp dependencies + let gulpDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp", "node_modules")).wait(); + _.each(gulpDependencies, dependency => { + assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); + }); + + // Get all gulp-jscs dependencies + let gulpJscsDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp-jscs", "node_modules")).wait(); + _.each(gulpJscsDependencies, dependency => { + assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); + }); + + // Get all gulp-jshint dependencies + let gulpJshintDependencies = fs.readDirectory(path.join(projectFolder, "node_modules", "gulp-jshint", "node_modules")).wait(); + _.each(gulpJshintDependencies, dependency => { + assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency)).wait()); + }); + }); }); \ No newline at end of file diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 3038102f2e..f5a56e7031 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -11,6 +11,7 @@ import StaticConfigLib = require("../lib/config"); import CommandsServiceLib = require("../lib/common/services/commands-service"); import optionsLib = require("../lib/options"); import hostInfoLib = require("../lib/common/host-info"); +import ProjectFilesManagerLib = require("../lib/services/project-files-manager"); import path = require("path"); import Future = require("fibers/future"); var assert = require("chai").assert; @@ -115,6 +116,7 @@ function createTestInjector() { }).future()(); } }); + testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index bf0ed7001a..7429a1416b 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -18,6 +18,7 @@ import ProjectDataLib = require("../lib/project-data"); import ProjectHelperLib = require("../lib/common/project-helper"); import optionsLib = require("../lib/options"); import hostInfoLib = require("../lib/common/host-info"); +import ProjectFilesManagerLib = require("../lib/services/project-files-manager"); import path = require("path"); import Future = require("fibers/future"); @@ -63,6 +64,7 @@ function createTestInjector() { return (() => { }).future()(); } }); + testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); return testInjector; } @@ -223,7 +225,10 @@ describe('Platform Service Tests', () => { return { appDestinationDirectoryPath: appDestFolderPath, appResourcesDestinationDirectoryPath: appResourcesFolderPath, - normalizedPlatformName: "iOS" + normalizedPlatformName: "iOS", + platformProjectService: { + prepareProject: () => Future.fromResult() + } } }; @@ -274,7 +279,10 @@ describe('Platform Service Tests', () => { return { appDestinationDirectoryPath: appDestFolderPath, appResourcesDestinationDirectoryPath: appResourcesFolderPath, - normalizedPlatformName: "Android" + normalizedPlatformName: "Android", + platformProjectService: { + prepareProject: () => Future.fromResult() + } } }; diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 781410308b..fdb5cd2083 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -17,6 +17,7 @@ import ProjectHelperLib = require("../lib/common/project-helper"); import PlatformsDataLib = require("../lib/platforms-data"); import ProjectDataServiceLib = require("../lib/services/project-data-service"); import helpers = require("../lib/common/helpers"); +import ProjectFilesManagerLib = require("../lib/services/project-files-manager"); import os = require("os"); import PluginsServiceLib = require("../lib/services/plugins-service"); @@ -64,6 +65,7 @@ function createTestInjector() { checkConsent: () => { return (() => { }).future()(); }, trackFeature: () => { return (() => { }).future()(); } }); + testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); return testInjector; } diff --git a/test/project-files-manager.ts b/test/project-files-manager.ts new file mode 100644 index 0000000000..8b4cf41997 --- /dev/null +++ b/test/project-files-manager.ts @@ -0,0 +1,81 @@ +/// +"use strict"; + +import yok = require('../lib/common/yok'); +import fsLib = require("../lib/common/file-system"); +import projectFilesManagerLib = require("../lib/services/project-files-manager"); +import hostInfoLib = require("../lib/common/host-info"); +import StaticConfigLib = require("../lib/config"); +import ErrorsLib = require("../lib/common/errors"); +import path = require("path"); +import temp = require("temp"); +temp.track(); + +var assert = require("chai").assert; + +function createTestInjector() { + let testInjector = new yok.Yok(); + testInjector.register("fs", fsLib.FileSystem); + testInjector.register("hostInfo", hostInfoLib.HostInfo); + testInjector.register("staticConfig", StaticConfigLib.StaticConfig); + testInjector.register("projectFilesManager", projectFilesManagerLib.ProjectFilesManager); + testInjector.register("errors", ErrorsLib.Errors); + testInjector.register("platformsData", { + platformsNames: ["ios", "android"] + }); + + return testInjector; +} + +function createFiles(testInjector: IInjector, filesToCreate: string[]): IFuture { + return (() => { + let fs = testInjector.resolve("fs"); + let directoryPath = temp.mkdirSync("Project Files Manager Tests"); + + _.each(filesToCreate, file => fs.writeFile(path.join(directoryPath, file), "").wait()); + + return directoryPath; + }).future()(); +} + +describe('Project Files Manager Tests', () => { + let testInjector: IInjector, projectFilesManager: IProjectFilesManager; + beforeEach(() => { + testInjector = createTestInjector(); + projectFilesManager = testInjector.resolve("projectFilesManager"); + }); + it("filters android specific files", () => { + let files = ["test.ios.x", "test.android.x"]; + let directoryPath = createFiles(testInjector, files).wait(); + + projectFilesManager.processPlatformSpecificFiles(directoryPath, "android").wait(); + + let fs = testInjector.resolve("fs"); + assert.isFalse(fs.exists(path.join(directoryPath, "test.ios.x")).wait()); + assert.isTrue(fs.exists(path.join(directoryPath, "test.x")).wait()); + assert.isFalse(fs.exists(path.join(directoryPath, "test.android.x")).wait()); + }); + it("filters ios specific files", () => { + let files = ["index.ios.html", "index1.android.html", "a.test"]; + let directoryPath = createFiles(testInjector, files).wait(); + + projectFilesManager.processPlatformSpecificFiles(directoryPath, "ios").wait(); + + let fs = testInjector.resolve("fs"); + assert.isFalse(fs.exists(path.join(directoryPath, "index1.android.html")).wait()); + assert.isFalse(fs.exists(path.join(directoryPath, "index1.html")).wait()); + assert.isTrue(fs.exists(path.join(directoryPath, "index.html")).wait()); + assert.isTrue(fs.exists(path.join(directoryPath, "a.test")).wait()); + }); + it("doesn't filter non platform specific files", () => { + let files = ["index1.js", "index2.js", "index3.js"]; + let directoryPath = createFiles(testInjector, files).wait(); + + projectFilesManager.processPlatformSpecificFiles(directoryPath, "ios").wait(); + + let fs = testInjector.resolve("fs"); + assert.isTrue(fs.exists(path.join(directoryPath, "index1.js")).wait()); + assert.isTrue(fs.exists(path.join(directoryPath, "index2.js")).wait()); + assert.isTrue(fs.exists(path.join(directoryPath, "index3.js")).wait()); + }); +}); \ No newline at end of file diff --git a/test/stubs.ts b/test/stubs.ts index a3b45db324..79b60c2a90 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -276,8 +276,8 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { afterCreateProject(projectRoot: string): IFuture { return Future.fromResult(); } - prepareProject(platformData: IPlatformData): IFuture { - return Future.fromResult(""); + prepareProject(): IFuture { + return Future.fromResult(); } buildProject(projectRoot: string): IFuture { return Future.fromResult(); @@ -297,6 +297,9 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { updatePlatform(currentVersion: string, newVersion: string): IFuture { return Future.fromResult(); } + prepareAppResources(appResourcesDirectoryPath: string): IFuture { + return Future.fromResult(); + } } export class ProjectDataService implements IProjectDataService {