Skip to content

Add tracking for the project types that users are creating and working with #2497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

/**
* 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<void>}
*/
trackProjectType(): Promise<void>;
}

interface IPlatformData {
Expand Down Expand Up @@ -183,4 +191,4 @@ interface INodeModulesDependenciesBuilder {
interface IBuildInfo {
prepareTime: string;
buildTime: string;
}
}
2 changes: 2 additions & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ interface IProjectData {
projectFilePath: string;
projectId?: string;
dependencies: any;
devDependencies: IStringDictionary;
appDirectoryPath: string;
appResourcesDirectoryPath: string;
projectType: string;
}

interface IProjectDataService {
Expand Down
43 changes: 43 additions & 0 deletions lib/project-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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();
}
Expand All @@ -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("."));
}
Expand Down
2 changes: 2 additions & 0 deletions lib/services/livesync/livesync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ class LiveSyncService implements ILiveSyncService {

@helpers.hook('livesync')
private async liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise<void>): Promise<void> {
await this.$platformService.trackProjectType();

let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise<void>)[] = [];

for (let dataItem of liveSyncData) {
Expand Down
24 changes: 22 additions & 2 deletions lib/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<void> {
let platformsDir = this.$projectData.platformsDir;
Expand Down Expand Up @@ -186,6 +189,8 @@ export class PlatformService implements IPlatformService {
public async preparePlatform(platform: string): Promise<boolean> {
this.validatePlatform(platform);

await this.trackProjectType();

//We need dev-dependencies here, so before-prepare hooks will be executed correctly.
try {
await this.$pluginsService.ensureAllDependenciesAreInstalled();
Expand Down Expand Up @@ -314,7 +319,7 @@ export class PlatformService implements IPlatformService {
}

public async shouldBuild(platform: string, buildConfig?: IBuildConfig): Promise<boolean> {
if (this.$projectChangesService.currentChanges.changesRequireBuild) {
if (this.$projectChangesService.currentChanges.changesRequireBuild) {
return true;
}
let platformData = this.$platformsData.getPlatformData(platform);
Expand Down Expand Up @@ -342,8 +347,21 @@ export class PlatformService implements IPlatformService {
return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime;
}

public async trackProjectType(): Promise<void> {
// 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<void> {
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);
Expand Down Expand Up @@ -417,6 +435,8 @@ export class PlatformService implements IPlatformService {
}

public async runPlatform(platform: string): Promise<void> {
await this.trackProjectType();

if (this.$options.justlaunch) {
this.$options.watch = false;
}
Expand Down
17 changes: 8 additions & 9 deletions lib/services/project-templates-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
let realTemplatePath: string;
// support <reserved_name>@<version> 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;
Expand Down
4 changes: 4 additions & 0 deletions test/npm-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions test/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
77 changes: 77 additions & 0 deletions test/project-data.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
26 changes: 18 additions & 8 deletions test/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -153,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");
Expand All @@ -166,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");
Expand All @@ -179,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");
Expand All @@ -192,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"));
});

Expand Down Expand Up @@ -436,6 +445,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);
Expand Down
Loading