Skip to content

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

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): IFuture<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 {IFuture<void>}
*/
trackProjectType(): IFuture<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 @@ -10,8 +10,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 @@ -99,6 +99,8 @@ class LiveSyncService implements ILiveSyncService {
@helpers.hook('livesync')
private liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture<void>): IFuture<void> {
return (() => {
this.$platformService.trackProjectType().wait();

let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => void)[] = [];
_.each(liveSyncData, (dataItem) => {
let service: IPlatformLiveSyncService = this.$injector.resolve("platformLiveSyncService", { _liveSyncData: dataItem });
Expand Down
31 changes: 26 additions & 5 deletions lib/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let clui = require("clui");
const buildInfoFileName = ".nsbuildinfo";

export class PlatformService implements IPlatformService {
private _trackedProjectFilePath: string = null;

constructor(private $devicesService: Mobile.IDevicesService,
private $errors: IErrors,
Expand All @@ -38,7 +39,8 @@ export class PlatformService implements IPlatformService {
private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory,
private $projectChangesService: IProjectChangesService,
private $emulatorPlatformService: IEmulatorPlatformService,
private $childProcess: IChildProcess) { }
private $childProcess: IChildProcess,
private $analyticsService: IAnalyticsService) { }

public addPlatforms(platforms: string[]): IFuture<void> {
return (() => {
Expand Down Expand Up @@ -196,6 +198,8 @@ export class PlatformService implements IPlatformService {
return (() => {
this.validatePlatform(platform);

this.trackProjectType().wait();

//We need dev-dependencies here, so before-prepare hooks will be executed correctly.
try {
this.$pluginsService.ensureAllDependenciesAreInstalled().wait();
Expand Down Expand Up @@ -348,7 +352,7 @@ export class PlatformService implements IPlatformService {
}
let platformData = this.$platformsData.getPlatformData(platform);
let forDevice = !buildConfig || buildConfig.buildForDevice;
let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath;
let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath;
if (!this.$fs.exists(outputPath)) {
return true;
}
Expand All @@ -372,9 +376,24 @@ export class PlatformService implements IPlatformService {
}).future<boolean>()();
}

public trackProjectType(): IFuture<void> {
return (() => {
// 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;

this.$analyticsService.track("Working with project type", this.$projectData.projectType).wait();
}
}).future<void>()();
}

public buildPlatform(platform: string, buildConfig?: IBuildConfig): IFuture<void> {
return (() => {
this.$logger.out("Building project...");

this.trackProjectType().wait();

let platformData = this.$platformsData.getPlatformData(platform);
platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig).wait();
let prepareInfo = this.$projectChangesService.getPrepareInfo(platform);
Expand Down Expand Up @@ -449,6 +468,8 @@ export class PlatformService implements IPlatformService {

public runPlatform(platform: string): IFuture<void> {
return (() => {
this.trackProjectType().wait();

if (this.$options.justlaunch) {
this.$options.watch = false;
}
Expand Down Expand Up @@ -484,7 +505,7 @@ export class PlatformService implements IPlatformService {
this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait();
let found: Mobile.IDeviceInfo[] = [];
if (this.$devicesService.hasDevices) {
found = this.$devicesService.getDevices().filter((device:Mobile.IDeviceInfo) => device.identifier === this.$options.device);
found = this.$devicesService.getDevices().filter((device: Mobile.IDeviceInfo) => device.identifier === this.$options.device);
}
if (found.length === 0) {
this.$errors.fail("Cannot find device with name: %s", this.$options.device);
Expand Down Expand Up @@ -514,7 +535,7 @@ export class PlatformService implements IPlatformService {
let deviceFilePath = this.getDeviceBuildInfoFilePath(device);
try {
return JSON.parse(this.readFile(device, deviceFilePath).wait());
} catch(e) {
} catch (e) {
return null;
};
}).future<IBuildInfo>()();
Expand All @@ -527,7 +548,7 @@ export class PlatformService implements IPlatformService {
try {
let buildInfoTime = this.$fs.readJson(buildInfoFile);
return buildInfoTime;
} catch(e) {
} catch (e) {
return null;
}
}
Expand Down
39 changes: 20 additions & 19 deletions lib/services/project-templates-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,32 @@ temp.track();
export class ProjectTemplatesService implements IProjectTemplatesService {

public constructor(private $errors: IErrors,
private $fs: IFileSystem,
private $logger: ILogger,
private $npm: INodePackageManager,
private $npmInstallationManager: INpmInstallationManager) { }
private $fs: IFileSystem,
private $logger: ILogger,
private $npm: INodePackageManager,
private $npmInstallationManager: INpmInstallationManager,
private $analyticsService: IAnalyticsService) { }

public prepareTemplate(originalTemplateName: string, projectDir: string): IFuture<string> {
return ((): string => {
let realTemplatePath: string;
if(originalTemplateName) {
let templateName = originalTemplateName || constants.RESERVED_TEMPLATE_NAMES["default"],
version: string = null;

if (originalTemplateName) {
// support <reserved_name>@<version> syntax
let data = originalTemplateName.split("@"),
name = data[0],
version = data[1];

if(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()]) {
realTemplatePath = this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()], version, projectDir).wait();
} else {
// Use the original template name, specified by user as it may be case-sensitive.
realTemplatePath = this.prepareNativeScriptTemplate(name, version, projectDir).wait();
}
} else {
realTemplatePath = this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], null/*version*/, projectDir).wait();
name = data[0];

version = data[1];

templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing case when template is case sensitive folder.
If the developer uses App as originalTemplateName and then uses app these can be two different apps. I'm OK with us not supporting this scenario, and either way there aren't such tests, but thought you should know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact the case is handled - when originalTemplateName is app, the name variable will be set to app.
At this point templateName will be set to app.

At another point, when user passes --template App, originalTemplateName will be set to App, the name variable will be set to App and the templateName will be set to App again.

}

if(realTemplatePath) {
this.$analyticsService.track("Template used for project creation", templateName).wait();

const realTemplatePath = this.prepareNativeScriptTemplate(templateName, version, projectDir).wait();

if (realTemplatePath) {
//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 All @@ -51,7 +52,7 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
*/
private prepareNativeScriptTemplate(templateName: string, version?: string, projectDir?: string): IFuture<string> {
this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`);
return this.$npmInstallationManager.install(templateName, projectDir, {version: version, dependencyType: "save"});
return this.$npmInstallationManager.install(templateName, projectDir, { version: version, dependencyType: "save" });
}
}
$injector.register("projectTemplatesService", ProjectTemplatesService);
4 changes: 4 additions & 0 deletions test/npm-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ function createTestInjector(): IInjector {
testInjector.register("config", StaticConfigLib.Configuration);
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
testInjector.register("analyticsService", {
track: () => Future.fromResult()
});

return testInjector;
}

Expand Down
5 changes: 5 additions & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { XmlValidator } from "../lib/xml-validator";
import * as ChildProcessLib from "../lib/common/child-process";
import {CleanCommand} from "../lib/commands/platform-clean";
import ProjectChangesLib = require("../lib/services/project-changes-service");
import Future = require("fibers/future");

let isCommandExecuted = true;

Expand Down Expand Up @@ -142,6 +143,10 @@ function createTestInjector() {
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
testInjector.register("analyticsService", {
track: () => Future.fromResult()
});

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 @@ -81,6 +81,9 @@ function createTestInjector() {
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
testInjector.register("analyticsService", {
track: () => Future.fromResult()
});

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");
});
});
});
Loading