From e947d4be71f6eae8f6f55826a59f45644dd56395 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Sun, 5 Feb 2017 20:31:39 +0200 Subject: [PATCH 1/2] Track from which template a project is created Add tracking from which template a project is created. This will give us better picture of the usage of CLI and the types of projects that the users are creating. --- lib/services/project-templates-service.ts | 17 ++++++++--------- test/project-service.ts | 2 ++ test/project-templates-service.ts | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index ba5ac17456..fb5292384d 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -5,25 +5,24 @@ temp.track(); export class ProjectTemplatesService implements IProjectTemplatesService { - public constructor(private $fs: IFileSystem, + public constructor(private $analyticsService: IAnalyticsService, + private $fs: IFileSystem, private $logger: ILogger, private $npmInstallationManager: INpmInstallationManager) { } public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise { - let realTemplatePath: string; // support @ syntax let data = originalTemplateName.split("@"), name = data[0], version = data[1]; - if (constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()]) { - realTemplatePath = await this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()], version, projectDir); - } else { - // Use the original template name, specified by user as it may be case-sensitive. - realTemplatePath = await this.prepareNativeScriptTemplate(originalTemplateName, version, projectDir); - } + const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name; - //this removes dependencies from templates so they are not copied to app folder + await this.$analyticsService.track("Template used for project creation", templateName); + + const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir); + + // this removes dependencies from templates so they are not copied to app folder this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME)); return realTemplatePath; diff --git a/test/project-service.ts b/test/project-service.ts index 38c24444e4..00b175ec18 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -114,6 +114,7 @@ class ProjectIntegrationTest { this.testInjector.register("fs", FileSystem); this.testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService); this.testInjector.register("staticConfig", StaticConfig); + this.testInjector.register("analyticsService", { track: async () => undefined }); this.testInjector.register("npmInstallationManager", NpmInstallationManager); this.testInjector.register("npm", NpmLib.NodePackageManager); @@ -436,6 +437,7 @@ function createTestInjector() { testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService); testInjector.register("staticConfig", StaticConfig); + testInjector.register("analyticsService", { track: async () => undefined }); testInjector.register("npmInstallationManager", NpmInstallationManager); testInjector.register("httpClient", HttpClientLib.HttpClient); diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index ff7589b4bb..6ec449d736 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -50,6 +50,8 @@ function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, np injector.register("projectTemplatesService", ProjectTemplatesService); + injector.register("analyticsService", { track: async () => undefined }); + return injector; } From db3bba59d443a58ff4daee0c38c132984731d235 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Mon, 6 Feb 2017 12:49:07 +0200 Subject: [PATCH 2/2] Track project type when deploy/run/livesync is executed Track the project type (Angular, Pure TypeScript, Pure JavaScript) when any of the commands is executed: * prepare * deploy * run * livesync This will allow us to better understand the type of projects that the users are building. --- lib/definitions/platform.d.ts | 10 ++- lib/definitions/project.d.ts | 2 + lib/project-data.ts | 43 +++++++++++++ lib/services/livesync/livesync-service.ts | 2 + lib/services/platform-service.ts | 24 ++++++- test/npm-support.ts | 4 ++ test/platform-commands.ts | 4 ++ test/platform-service.ts | 3 + test/project-data.ts | 77 +++++++++++++++++++++++ test/project-service.ts | 24 ++++--- test/stubs.ts | 6 ++ 11 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 test/project-data.ts diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 0ab71ba8e3..624802f7d4 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -142,6 +142,14 @@ interface IPlatformService { * @returns {string} The contents of the file or null when there is no such file. */ readFile(device: Mobile.IDevice, deviceFilePath: string): Promise; + + /** + * Sends information to analytics for current project type. + * The information is sent once per process for each project. + * In long living process, where the project may change, each of the projects will be tracked after it's being opened. + * @returns {Promise} + */ + trackProjectType(): Promise; } interface IPlatformData { @@ -183,4 +191,4 @@ interface INodeModulesDependenciesBuilder { interface IBuildInfo { prepareTime: string; buildTime: string; -} \ No newline at end of file +} diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index cc37a3c2de..dbda895226 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -53,8 +53,10 @@ interface IProjectData { projectFilePath: string; projectId?: string; dependencies: any; + devDependencies: IStringDictionary; appDirectoryPath: string; appResourcesDirectoryPath: string; + projectType: string; } interface IProjectDataService { diff --git a/lib/project-data.ts b/lib/project-data.ts index a2e113902b..e56c258c64 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -2,9 +2,33 @@ import * as constants from "./constants"; import * as path from "path"; import { EOL } from "os"; +interface IProjectType { + type: string; + requiredDependencies?: string[]; + isDefaultProjectType?: boolean; +} + export class ProjectData implements IProjectData { private static OLD_PROJECT_FILE_NAME = ".tnsproject"; + /** + * NOTE: Order of the elements is important as the TypeScript dependencies are commonly included in Angular project as well. + */ + private static PROJECT_TYPES: IProjectType[] = [ + { + type: "Pure JavaScript", + isDefaultProjectType: true + }, + { + type: "Angular", + requiredDependencies: ["@angular/core", "nativescript-angular"] + }, + { + type: "Pure TypeScript", + requiredDependencies: ["typescript", "nativescript-dev-typescript"] + } + ]; + public projectDir: string; public platformsDir: string; public projectFilePath: string; @@ -13,6 +37,8 @@ export class ProjectData implements IProjectData { public appDirectoryPath: string; public appResourcesDirectoryPath: string; public dependencies: any; + public devDependencies: IStringDictionary; + public projectType: string; constructor(private $fs: IFileSystem, private $errors: IErrors, @@ -48,6 +74,8 @@ export class ProjectData implements IProjectData { if (data) { this.projectId = data.id; this.dependencies = fileContent.dependencies; + this.devDependencies = fileContent.devDependencies; + this.projectType = this.getProjectType(); } else { // This is the case when we have package.json file but nativescipt key is not presented in it this.tryToUpgradeProject(); } @@ -57,6 +85,21 @@ export class ProjectData implements IProjectData { } } + private getProjectType(): string { + let detectedProjectType = _.find(ProjectData.PROJECT_TYPES, (projectType) => projectType.isDefaultProjectType).type; + + const deps: string[] = _.keys(this.dependencies).concat(_.keys(this.devDependencies)); + + _.each(ProjectData.PROJECT_TYPES, projectType => { + if (_.some(projectType.requiredDependencies, requiredDependency => deps.indexOf(requiredDependency) !== -1)) { + detectedProjectType = projectType.type; + return false; + } + }); + + return detectedProjectType; + } + private throwNoProjectFoundError(): void { this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", this.$options.path || path.resolve(".")); } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 3e8830726f..5dfb10f52a 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -102,6 +102,8 @@ class LiveSyncService implements ILiveSyncService { @helpers.hook('livesync') private async liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise): Promise { + await this.$platformService.trackProjectType(); + let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise)[] = []; for (let dataItem of liveSyncData) { diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 292c80da3d..c55fcc531d 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -17,6 +17,8 @@ export class PlatformService implements IPlatformService { return this.$hooksService; } + private _trackedProjectFilePath: string = null; + constructor(private $devicesService: Mobile.IDevicesService, private $errors: IErrors, private $fs: IFileSystem, @@ -38,7 +40,8 @@ export class PlatformService implements IPlatformService { private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, private $projectChangesService: IProjectChangesService, - private $emulatorPlatformService: IEmulatorPlatformService) { } + private $emulatorPlatformService: IEmulatorPlatformService, + private $analyticsService: IAnalyticsService) { } public async addPlatforms(platforms: string[]): Promise { let platformsDir = this.$projectData.platformsDir; @@ -186,6 +189,8 @@ export class PlatformService implements IPlatformService { public async preparePlatform(platform: string): Promise { this.validatePlatform(platform); + await this.trackProjectType(); + //We need dev-dependencies here, so before-prepare hooks will be executed correctly. try { await this.$pluginsService.ensureAllDependenciesAreInstalled(); @@ -314,7 +319,7 @@ export class PlatformService implements IPlatformService { } public async shouldBuild(platform: string, buildConfig?: IBuildConfig): Promise { - if (this.$projectChangesService.currentChanges.changesRequireBuild) { + if (this.$projectChangesService.currentChanges.changesRequireBuild) { return true; } let platformData = this.$platformsData.getPlatformData(platform); @@ -342,8 +347,21 @@ export class PlatformService implements IPlatformService { return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; } + public async trackProjectType(): Promise { + // Track each project once per process. + // In long living process, where we may work with multiple projects, we would like to track the information for each of them. + if (this.$projectData && (this.$projectData.projectFilePath !== this._trackedProjectFilePath)) { + this._trackedProjectFilePath = this.$projectData.projectFilePath; + + await this.$analyticsService.track("Working with project type", this.$projectData.projectType); + } + } + public async buildPlatform(platform: string, buildConfig?: IBuildConfig): Promise { this.$logger.out("Building project..."); + + await this.trackProjectType(); + let platformData = this.$platformsData.getPlatformData(platform); await platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig); let prepareInfo = this.$projectChangesService.getPrepareInfo(platform); @@ -417,6 +435,8 @@ export class PlatformService implements IPlatformService { } public async runPlatform(platform: string): Promise { + await this.trackProjectType(); + if (this.$options.justlaunch) { this.$options.watch = false; } diff --git a/test/npm-support.ts b/test/npm-support.ts index b69cd5e47d..652cf169f2 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -79,6 +79,10 @@ function createTestInjector(): IInjector { testInjector.register("config", StaticConfigLib.Configuration); testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); + testInjector.register("analyticsService", { + track: async () => undefined + }); + return testInjector; } diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 2a77198ef9..a868c17321 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -136,6 +136,10 @@ function createTestInjector() { testInjector.register("childProcess", ChildProcessLib.ChildProcess); testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); + testInjector.register("analyticsService", { + track: async () => undefined + }); + return testInjector; } diff --git a/test/platform-service.ts b/test/platform-service.ts index 806d290c33..e9da8636dd 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -76,6 +76,9 @@ function createTestInjector() { testInjector.register("childProcess", ChildProcessLib.ChildProcess); testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); + testInjector.register("analyticsService", { + track: async () => undefined + }); return testInjector; } diff --git a/test/project-data.ts b/test/project-data.ts new file mode 100644 index 0000000000..b02399d165 --- /dev/null +++ b/test/project-data.ts @@ -0,0 +1,77 @@ +import { ProjectData } from "../lib/project-data"; +import { Yok } from "../lib/common/yok"; +import { assert } from "chai"; +import * as stubs from "./stubs"; +import * as path from "path"; + +describe("projectData", () => { + const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + + testInjector.register("projectHelper", { + projectDir: null, + sanitizeName: (name: string) => name + }); + + testInjector.register("fs", { + exists: () => true, + readJson: (): any => null + }); + + testInjector.register("staticConfig", { + CLIENT_NAME_KEY_IN_PROJECT_FILE: "nativescript", + PROJECT_FILE_NAME: "package.json" + }); + + testInjector.register("errors", stubs.ErrorsStub); + + testInjector.register("logger", stubs.LoggerStub); + + testInjector.register("options", {}); + + testInjector.register("projectData", ProjectData); + + return testInjector; + }; + + describe("projectType", () => { + + const assertProjectType = (dependencies: any, devDependencies: any, expectedProjecType: string) => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = (filePath: string) => filePath && path.basename(filePath) === "package.json"; + + fs.readJson = () => ({ + nativescript: {}, + dependencies: dependencies, + devDependencies: devDependencies + }); + + const projectHelper: IProjectHelper = testInjector.resolve("projectHelper"); + projectHelper.projectDir = "projectDir"; + + const projectData: IProjectData = testInjector.resolve("projectData"); + assert.deepEqual(projectData.projectType, expectedProjecType); + }; + + it("detects project as Angular when @angular/core exists as dependency", () => { + assertProjectType({ "@angular/core": "*" }, null, "Angular"); + }); + + it("detects project as Angular when nativescript-angular exists as dependency", () => { + assertProjectType({ "nativescript-angular": "*" }, null, "Angular"); + }); + + it("detects project as TypeScript when nativescript-dev-typescript exists as dependency", () => { + assertProjectType(null, { "nativescript-dev-typescript": "*" }, "Pure TypeScript"); + }); + + it("detects project as TypeScript when typescript exists as dependency", () => { + assertProjectType(null, { "typescript": "*" }, "Pure TypeScript"); + }); + + it("detects project as JavaScript when no other project type is detected", () => { + assertProjectType(null, null, "Pure JavaScript"); + }); + }); +}); diff --git a/test/project-service.ts b/test/project-service.ts index 00b175ec18..749f709027 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -154,8 +154,10 @@ describe("Project Service Tests", () => { "readme": "dummy", "repository": "dummy" }); - await npmInstallationManager.install("tns-template-hello-world", defaultTemplateDir, { dependencyType: "save" }); - defaultTemplatePath = path.join(defaultTemplateDir, "node_modules", "tns-template-hello-world"); + + await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultTemplateDir, { dependencyType: "save" }); + defaultTemplatePath = path.join(defaultTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]); + fs.deleteDirectory(path.join(defaultTemplatePath, "node_modules")); let defaultSpecificVersionTemplateDir = temp.mkdirSync("defaultTemplateSpeciffic"); @@ -167,8 +169,10 @@ describe("Project Service Tests", () => { "readme": "dummy", "repository": "dummy" }); - await npmInstallationManager.install("tns-template-hello-world", defaultSpecificVersionTemplateDir, { version: "1.4.0", dependencyType: "save" }); - defaultSpecificVersionTemplatePath = path.join(defaultSpecificVersionTemplateDir, "node_modules", "tns-template-hello-world"); + + await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultSpecificVersionTemplateDir, { version: "1.4.0", dependencyType: "save" }); + defaultSpecificVersionTemplatePath = path.join(defaultSpecificVersionTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]); + fs.deleteDirectory(path.join(defaultSpecificVersionTemplatePath, "node_modules")); let angularTemplateDir = temp.mkdirSync("angularTemplate"); @@ -180,8 +184,10 @@ describe("Project Service Tests", () => { "readme": "dummy", "repository": "dummy" }); - await npmInstallationManager.install("tns-template-hello-world-ng", angularTemplateDir, { dependencyType: "save" }); - angularTemplatePath = path.join(angularTemplateDir, "node_modules", "tns-template-hello-world-ng"); + + await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["angular"], angularTemplateDir, { dependencyType: "save" }); + angularTemplatePath = path.join(angularTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["angular"]); + fs.deleteDirectory(path.join(angularTemplatePath, "node_modules")); let typescriptTemplateDir = temp.mkdirSync("typescriptTemplate"); @@ -193,8 +199,10 @@ describe("Project Service Tests", () => { "readme": "dummy", "repository": "dummy" }); - await npmInstallationManager.install("tns-template-hello-world-ts", typescriptTemplateDir, { dependencyType: "save" }); - typescriptTemplatePath = path.join(typescriptTemplateDir, "node_modules", "tns-template-hello-world-ts"); + + await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["typescript"], typescriptTemplateDir, { dependencyType: "save" }); + typescriptTemplatePath = path.join(typescriptTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["typescript"]); + fs.deleteDirectory(path.join(typescriptTemplatePath, "node_modules")); }); diff --git a/test/stubs.ts b/test/stubs.ts index 9ceceb260c..7f576a6a5e 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -235,6 +235,8 @@ export class ProjectDataStub implements IProjectData { dependencies: any; appDirectoryPath: string; appResourcesDirectoryPath: string; + devDependencies: IStringDictionary; + projectType: string; } export class PlatformsDataStub implements IPlatformsData { @@ -657,6 +659,10 @@ export class PlatformServiceStub implements IPlatformService { public readFile(device: Mobile.IDevice, deviceFilePath: string): Promise { return Promise.resolve(""); } + + public async trackProjectType(): Promise { + return null; + } } export class EmulatorPlatformService implements IEmulatorPlatformService {