diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index e46818651d..7724e5899a 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -33,5 +33,6 @@ $injector.requireCommand("emulate", "./commands/emulate"); $injector.requireCommand("list-devices", "./commands/list-devices"); $injector.require("npm", "./node-package-manager"); +$injector.require("lockfile", "./lockfile"); $injector.require("config", "./config"); $injector.require("optionsService", "./services/options-service"); diff --git a/lib/common b/lib/common index 33b81c2682..fa0549c52d 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 33b81c26827c3f872be810789a987a6061452efb +Subproject commit fa0549c52dbe8e36222ae24a39e0b0b65e27ccb0 diff --git a/lib/declarations.ts b/lib/declarations.ts index 951a9712f9..cb1f127f35 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -1,5 +1,5 @@ interface INodePackageManager { - getCacheRootPath(): IFuture; + getCacheRootPath(): string; addToCache(packageName: string, version: string): IFuture; cacheUnpack(packageName: string, version: string, unpackTarget?: string): IFuture; load(config?: any): IFuture; @@ -21,3 +21,7 @@ interface IApplicationPackage { time: Date; } +interface ILockFile { + lock(): IFuture; + unlock(): IFuture; +} \ No newline at end of file diff --git a/lib/definitions/lockfile.d.ts b/lib/definitions/lockfile.d.ts new file mode 100644 index 0000000000..80b330b9cf --- /dev/null +++ b/lib/definitions/lockfile.d.ts @@ -0,0 +1,15 @@ +declare module "lockfile" { + export function lock(lockFilename: string, lockParams: ILockParams, callback: (err: Error) => void): void; + export function lockSync(lockFilename: string, lockParams: ILockSyncParams): void; + export function unlock(lockFilename: string, callback: (err: Error) => void): void; + export function unlockSync(lockFilename: string): void; + + interface ILockSyncParams { + retries: number; + stale: number; + } + + interface ILockParams extends ILockSyncParams { + retryWait: number; + } +} diff --git a/lib/lockfile.ts b/lib/lockfile.ts new file mode 100644 index 0000000000..9851fe5480 --- /dev/null +++ b/lib/lockfile.ts @@ -0,0 +1,42 @@ +/// +"use strict"; + +import Future = require("fibers/future"); +import lockfile = require("lockfile"); +import path = require("path"); +import options = require("./options"); + +export class LockFile implements ILockFile { + private static LOCK_FILENAME = path.join(options["profile-dir"], ".lock"); + private static LOCK_EXPIRY_PERIOD_SEC = 180; + private static LOCK_PARAMS = { + retryWait: 100, + retries: LockFile.LOCK_EXPIRY_PERIOD_SEC*10, + stale: LockFile.LOCK_EXPIRY_PERIOD_SEC*1000 + }; + + public lock(): IFuture { + var future = new Future(); + lockfile.lock(LockFile.LOCK_FILENAME, LockFile.LOCK_PARAMS, (err: Error) => { + if(err) { + future.throw(err); + } else { + future.return(); + } + }); + return future; + } + + public unlock(): IFuture { + var future = new Future(); + lockfile.unlock(LockFile.LOCK_FILENAME, (err: Error) => { + if(err) { + future.throw(err); + } else { + future.return(); + } + }); + return future; + } +} +$injector.register("lockfile", LockFile); \ No newline at end of file diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index 0ca9cfa6f7..0ac16f7c9a 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -1,6 +1,5 @@ /// "use strict"; - import path = require("path"); require("./bootstrap"); diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts index bc8c3b3917..5902dbd2ad 100644 --- a/lib/node-package-manager.ts +++ b/lib/node-package-manager.ts @@ -14,28 +14,22 @@ export class NodePackageManager implements INodePackageManager { private static NPM_REGISTRY_URL = "http://registry.npmjs.org/"; private versionsCache: IDictionary; - private isLoaded: boolean; constructor(private $logger: ILogger, private $errors: IErrors, private $httpClient: Server.IHttpClient, - private $staticConfig: IStaticConfig, - private $fs: IFileSystem) { + private $fs: IFileSystem, + private $lockfile: ILockFile) { this.versionsCache = {}; + this.load().wait(); } - public getCacheRootPath(): IFuture { - return (() => { - this.load().wait(); - return npm.cache; - }).future()(); + public getCacheRootPath(): string { + return npm.cache; } public addToCache(packageName: string, version: string): IFuture { - return (() => { - this.load().wait(); - this.addToCacheCore(packageName, version).wait(); - }).future()(); + return this.addToCacheCore(packageName, version); } public load(config?: any): IFuture { @@ -52,29 +46,21 @@ export class NodePackageManager implements INodePackageManager { public install(packageName: string, opts?: INpmInstallOptions): IFuture { return (() => { - try { - this.load().wait(); // It's obligatory to execute load before whatever npm function + this.$lockfile.lock().wait(); + try { var packageToInstall = packageName; var pathToSave = (opts && opts.pathToSave) || npm.cache; var version = (opts && opts.version) || null; - var isSemanticVersioningDisabled = options.frameworkPath ? true : false; // We need to disable sem versioning for local packages - - if(version) { - this.validateVersion(packageName, version).wait(); - packageToInstall = packageName + "@" + version; - } - this.installCore(packageToInstall, pathToSave, isSemanticVersioningDisabled).wait(); + return this.installCore(packageToInstall, pathToSave, version).wait(); } catch(error) { this.$logger.debug(error); - this.$errors.fail(NodePackageManager.NPM_LOAD_FAILED); + this.$errors.fail("%s. Error: %s", NodePackageManager.NPM_LOAD_FAILED, error); + } finally { + this.$lockfile.unlock().wait(); } - var pathToNodeModules = path.join(pathToSave, "node_modules"); - var folders = this.$fs.readDirectory(pathToNodeModules).wait(); - return path.join(pathToNodeModules, folders[0]); - }).future()(); } @@ -86,21 +72,39 @@ export class NodePackageManager implements INodePackageManager { }).future()(); } - private installCore(packageName: string, pathToSave: string, isSemanticVersioningDisabled: boolean): IFuture { - var currentVersion = this.$staticConfig.version; - if(!semver.valid(currentVersion)) { - this.$errors.fail("Invalid version."); - } + private installCore(packageName: string, pathToSave: string, version: string): IFuture { + return (() => { + if (options.frameworkPath) { + if (this.$fs.getFsStats(options.frameworkPath).wait().isFile()) { + this.npmInstall(packageName, pathToSave, version).wait(); + var pathToNodeModules = path.join(pathToSave, "node_modules"); + var folders = this.$fs.readDirectory(pathToNodeModules).wait(); + return path.join(pathToNodeModules, folders[0]); + } + return options.frameworkPath; + } else { + var version = version || this.getLatestVersion(packageName).wait(); + var packagePath = path.join(npm.cache, packageName, version, "package"); + if (!this.isPackageCached(packagePath).wait()) { + this.addToCacheCore(packageName, version).wait(); + } - if(!isSemanticVersioningDisabled) { - var incrementedVersion = semver.inc(currentVersion, constants.ReleaseType.MINOR); - if(packageName.indexOf("@") < 0) { - packageName = packageName + "@<" + incrementedVersion; + if(!this.isPackageUnpacked(packagePath).wait()) { + this.cacheUnpack(packageName, version).wait(); + } + return packagePath; } - } + }).future()(); + } + private npmInstall(packageName: string, pathToSave: string, version: string): IFuture { this.$logger.out("Installing ", packageName); + var incrementedVersion = semver.inc(version, constants.ReleaseType.MINOR); + if (!options.frameworkPath && packageName.indexOf("@") < 0) { + packageName = packageName + "@<" + incrementedVersion; + } + var future = new Future(); npm.commands["install"](pathToSave, packageName, (err: Error, data: any) => { if(err) { @@ -113,6 +117,17 @@ export class NodePackageManager implements INodePackageManager { return future; } + private isPackageCached(packagePath: string): IFuture { + return this.$fs.exists(packagePath); + } + + private isPackageUnpacked(packagePath: string): IFuture { + return (() => { + return this.$fs.getFsStats(packagePath).wait().isDirectory() && + helpers.enumerateFilesInDirectorySync(packagePath).length > 1; + }).future()(); + } + private addToCacheCore(packageName: string, version: string): IFuture { var future = new Future(); npm.commands["cache"].add(packageName, version, undefined, (err: Error, data: any) => { @@ -150,14 +165,5 @@ export class NodePackageManager implements INodePackageManager { return this.versionsCache[packageName]; }).future()(); } - - private validateVersion(packageName: string, version: string): IFuture { - return (() => { - var versions = this.getAvailableVersions(packageName).wait(); - if(!_.contains(versions, version)) { - this.$errors.fail("Invalid version. Valid versions are: %s", helpers.formatListOfNames(versions, "and")); - } - }).future()(); - } } $injector.register("npm", NodePackageManager); diff --git a/lib/options.ts b/lib/options.ts index cf438f7f67..9ad4681303 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -13,6 +13,7 @@ var knownOpts:any = { "release": Boolean, "device": Boolean, "emulator": Boolean, + "symlink": Boolean, "keyStorePath": String, "keyStorePassword": String, "keyStoreAlias": String, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 5a27db3499..a760d576f4 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -47,13 +47,20 @@ class AndroidProjectService implements IPlatformProjectService { public createProject(projectRoot: string, frameworkDir: string): IFuture { return (() => { + this.$fs.ensureDirectoryExists(projectRoot).wait(); + this.validateAndroidTarget(frameworkDir); // We need framework to be installed to validate android target so we can't call this method in validate() - var paths = "assets libs res".split(' ').map(p => path.join(frameworkDir, p)); - shell.cp("-R", paths, projectRoot); + if(options.symlink) { + this.copy(projectRoot, frameworkDir, "res", "-R").wait(); + this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties", "-f").wait(); - paths = ".project AndroidManifest.xml project.properties".split(' ').map(p => path.join(frameworkDir, p)); - shell.cp("-f", paths, projectRoot); + this.symlinkDirectory("assets", projectRoot, frameworkDir).wait(); + this.symlinkDirectory("libs", projectRoot, frameworkDir).wait(); + } else { + this.copy(projectRoot, frameworkDir, "assets libs res", "-R").wait(); + this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties", "-f").wait(); + } // Create src folder var packageName = this.$projectData.projectId; @@ -124,6 +131,13 @@ class AndroidProjectService implements IPlatformProjectService { return [".jar", ".dat"]; } + private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): IFuture { + return (() => { + var paths = files.split(' ').map(p => path.join(frameworkDir, p)); + shell.cp(cpArg, paths, projectRoot); + }).future()(); + } + private spawn(command: string, args: string[]): IFuture { if (hostInfo.isWindows()) { args.unshift('/s', '/c', command); @@ -238,5 +252,23 @@ class AndroidProjectService implements IPlatformProjectService { } }).future()(); } + + private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture { + return (() => { + this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait(); + var directoryContent = this.$fs.readDirectory(path.join(frameworkDir, directoryName)).wait(); + + _.each(directoryContent, (file: string) => { + var sourceFilePath = path.join(frameworkDir, directoryName, file); + var destinationFilePath = path.join(projectRoot, directoryName, file); + if(this.$fs.getFsStats(sourceFilePath).wait().isFile()) { + this.$fs.symlink(sourceFilePath, destinationFilePath).wait(); + } else { + this.$fs.symlink(sourceFilePath, destinationFilePath, "dir").wait(); + } + }); + + }).future()(); + } } $injector.register("androidProjectService", AndroidProjectService); \ No newline at end of file diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 76d001cfaa..9e43ec804b 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -62,7 +62,22 @@ class IOSProjectService implements IPlatformProjectService { public createProject(projectRoot: string, frameworkDir: string): IFuture { return (() => { - shell.cp("-R", path.join(frameworkDir, "*"), projectRoot); + if(options.symlink) { + this.$fs.ensureDirectoryExists(path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)).wait(); + var xcodeProjectName = util.format("%s.xcodeproj", IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER); + + shell.cp("-R", path.join(frameworkDir, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, "*"), path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)); + shell.cp("-R", path.join(frameworkDir, xcodeProjectName), path.join(projectRoot)); + + var directoryContent = this.$fs.readDirectory(frameworkDir).wait(); + var frameworkFiles = _.difference(directoryContent, [IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, xcodeProjectName]); + _.each(frameworkFiles, (file: string) => { + this.$fs.symlink(path.join(frameworkDir, file), path.join(projectRoot, file)).wait(); + }); + + } else { + shell.cp("-R", path.join(frameworkDir, "*"), projectRoot); + } }).future()(); } diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 6598df3a92..65a524cd0d 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -91,9 +91,11 @@ export class PlatformService implements IPlatformService { platformData.platformProjectService.createProject(platformData.projectRoot, frameworkDir).wait(); var installedVersion = this.$fs.readJson(path.join(frameworkDir, "../", "package.json")).wait().version; - // Need to remove unneeded node_modules folder - // One level up is the runtime module and one above is the node_modules folder. - this.$fs.deleteDirectory(path.join(frameworkDir, "../../")).wait(); + if(options.frameworkPath && !options.symlink) { + // Need to remove unneeded node_modules folder + // One level up is the runtime module and one above is the node_modules folder. + this.$fs.deleteDirectory(path.join(frameworkDir, "../../")).wait(); + } platformData.platformProjectService.interpolateData(platformData.projectRoot).wait(); platformData.platformProjectService.afterCreateProject(platformData.projectRoot).wait(); @@ -405,7 +407,7 @@ export class PlatformService implements IPlatformService { // Add new framework files var newFrameworkFiles = this.getFrameworkFiles(platformData, newVersion).wait(); - var cacheDirectoryPath = this.getNpmCacheDirectoryCore(platformData.frameworkPackageName, newVersion).wait(); + var cacheDirectoryPath = this.getNpmCacheDirectoryCore(platformData.frameworkPackageName, newVersion); _.each(newFrameworkFiles, file => { shell.cp("-f", path.join(cacheDirectoryPath, file), path.join(platformData.projectRoot, file)); }); @@ -433,7 +435,7 @@ export class PlatformService implements IPlatformService { private getNpmCacheDirectory(packageName: string, version: string): IFuture { return (() => { - var npmCacheDirectoryPath = this.getNpmCacheDirectoryCore(packageName, version).wait(); + var npmCacheDirectoryPath = this.getNpmCacheDirectoryCore(packageName, version); if(!this.$fs.exists(npmCacheDirectoryPath).wait()) { this.$npm.addToCache(packageName, version).wait(); @@ -443,11 +445,8 @@ export class PlatformService implements IPlatformService { }).future()(); } - private getNpmCacheDirectoryCore(packageName: string, version: string): IFuture { - return (() => { - var npmCacheRoot = this.$npm.getCacheRootPath().wait(); - return path.join(npmCacheRoot, packageName, version, "package"); - }).future()(); + private getNpmCacheDirectoryCore(packageName: string, version: string): string { + return path.join(this.$npm.getCacheRootPath(), packageName, version, "package"); } } $injector.register("platformService", PlatformService); diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index e4c3976a1f..bb869d1ff6 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -43,14 +43,14 @@ export class ProjectService implements IProjectService { var appDirectory = path.join(projectDir, constants.APP_FOLDER_NAME); var appPath: string = null; - if(customAppPath) { + if (customAppPath) { this.$logger.trace("Using custom app from %s", customAppPath); // Make sure that the source app/ is not a direct ancestor of a target app/ var relativePathFromSourceToTarget = path.relative(customAppPath, appDirectory); // path.relative returns second argument if the paths are located on different disks // so in this case we don't need to make the check for direct ancestor - if(relativePathFromSourceToTarget !== appDirectory) { + if (relativePathFromSourceToTarget !== appDirectory) { var doesRelativePathGoUpAtLeastOneDir = relativePathFromSourceToTarget.split(path.sep)[0] === ".."; if (!doesRelativePathGoUpAtLeastOneDir) { this.$errors.fail("Project dir %s must not be created at/inside the template used to create the project %s.", projectDir, customAppPath); @@ -67,28 +67,29 @@ export class ProjectService implements IProjectService { } try { - this.createProjectCore(projectDir, appPath, projectId, false).wait(); - } catch(err) { + this.createProjectCore(projectDir, appPath, projectId).wait(); + } catch (err) { this.$fs.deleteDirectory(projectDir).wait(); throw err; } + this.$logger.out("Project %s was successfully created", projectName); }).future()(); } - private createProjectCore(projectDir: string, appPath: string, projectId: string, symlink?: boolean): IFuture { + private createProjectCore(projectDir: string, appSourcePath: string, projectId: string): IFuture { return (() => { - if(!this.$fs.exists(projectDir).wait()) { - this.$fs.createDirectory(projectDir).wait(); - } - if(symlink) { - // TODO: Implement support for symlink the app folder instead of copying + this.$fs.ensureDirectoryExists(projectDir).wait(); + + var appDestinationPath = path.join(projectDir, constants.APP_FOLDER_NAME); + this.$fs.createDirectory(appDestinationPath).wait(); + + if(options.symlink) { + this.$fs.symlink(appSourcePath, appDestinationPath).wait(); } else { - var appDir = path.join(projectDir, constants.APP_FOLDER_NAME); - this.$fs.createDirectory(appDir).wait(); - shell.cp('-R', path.join(appPath, "*"), appDir); + shell.cp('-R', path.join(appSourcePath, "*"), appDestinationPath); } this.createBasicProjectStructure(projectDir, projectId).wait(); }).future()(); diff --git a/package.json b/package.json index c1334174c7..e8fb827bcc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "fibers": "https://github.com/icenium/node-fibers/tarball/master", "filesize": "2.0.3", "iconv-lite": "0.4.4", + "lockfile": "1.0.0", "log4js": "0.6.9", "mkdirp": "0.3.5", "mute-stream": "0.0.4", diff --git a/resources/help.txt b/resources/help.txt index 2e01972f93..a05fe38414 100644 --- a/resources/help.txt +++ b/resources/help.txt @@ -110,6 +110,7 @@ Configures the current project to target the selected platform. When you add a t Options: --frameworkPath - specifies the path to local runtime. It should be npm package. + --symlink - symlink the framework into the project. If this option is used with --frameworkPath, --frameworkPath should specify a path to folder. In this version of the Telerik NativeScript CLI, you can target iOS and Android, based on your system. You need to have your system configured for development with the target platform. On Windows systems, you can target Android. diff --git a/test/platform-commands.ts b/test/platform-commands.ts index b23fcd75f8..02d7263aa0 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -77,6 +77,7 @@ function createTestInjector() { testInjector.registerCommand("platform|add", PlatformAddCommandLib.AddPlatformCommand); testInjector.registerCommand("platform|remove", PlatformRemoveCommandLib.RemovePlatformCommand); testInjector.registerCommand("platform|update", PlatformUpdateCommandLib.UpdatePlatformCommand); + testInjector.register("lockfile", { }); return testInjector; diff --git a/test/platform-service.ts b/test/platform-service.ts index c483e4e395..467fff3705 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -37,6 +37,7 @@ function createTestInjector() { testInjector.register('androidEmulatorServices', {}); testInjector.register('projectDataService', stubs.ProjectDataService); testInjector.register('prompter', {}); + testInjector.register('lockfile', stubs.LockFile); return testInjector; } @@ -151,7 +152,7 @@ describe('Platform Service Tests', () => { it("should fail when the versions are the same", () => { var npm: INodePackageManager = testInjector.resolve("npm"); npm.getLatestVersion = () => (() => "0.2.0").future()(); - npm.getCacheRootPath = () => (() => "").future()(); + npm.getCacheRootPath = () => ""; (() => platformService.updatePlatforms(["android"]).wait()).should.throw(); }); diff --git a/test/project-service.ts b/test/project-service.ts index 34988db92a..61f21008eb 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -43,7 +43,7 @@ class ProjectIntegrationTest { var fs = this.testInjector.resolve("fs"); var defaultTemplatePackageName = "tns-template-hello-world"; - var cacheRoot = npm.getCacheRootPath().wait(); + var cacheRoot = npm.getCacheRootPath(); var defaultTemplatePath = path.join(cacheRoot, defaultTemplatePackageName); var latestVersion = npm.getLatestVersion(defaultTemplatePackageName).wait(); @@ -109,6 +109,7 @@ class ProjectIntegrationTest { this.testInjector.register("npm", NpmLib.NodePackageManager); this.testInjector.register("httpClient", HttpClientLib.HttpClient); this.testInjector.register("config", {}); + this.testInjector.register("lockfile", stubs.LockFile); } } diff --git a/test/stubs.ts b/test/stubs.ts index c8e1b2dbf4..12d18c4ea0 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -116,7 +116,7 @@ export class FileSystemStub implements IFileSystem { return undefined; } - symlink(sourePath: string, destinationPath: string): IFuture { + symlink(sourcePath: string, destinationPath: string): IFuture { return undefined; } @@ -149,7 +149,7 @@ export class ErrorsStub implements IErrors { } export class NPMStub implements INodePackageManager { - getCacheRootPath(): IFuture { + getCacheRootPath(): string { return undefined; } @@ -277,3 +277,14 @@ export class HooksServiceStub implements IHooksService { return (() => { }).future()(); } } + +export class LockFile { + lock(): IFuture { + return (() => {}).future()(); + } + + unlock(): IFuture { + return (() => {}).future()(); + } +} +