Skip to content

feat: Speed up adding native platform #3735

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
Jul 9, 2018
Merged
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
4 changes: 1 addition & 3 deletions lib/npm-installation-manager.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ export class NpmInstallationManager implements INpmInstallationManager {
constructor(private $npm: INodePackageManager,
private $childProcess: IChildProcess,
private $logger: ILogger,
private $options: IOptions,
private $settingsService: ISettingsService,
private $fs: IFileSystem,
private $staticConfig: IStaticConfig,
@@ -39,9 +38,8 @@ export class NpmInstallationManager implements INpmInstallationManager {
return maxSatisfying || latestVersion;
}

public async install(packageName: string, projectDir: string, opts?: INpmInstallOptions): Promise<any> {
public async install(packageToInstall: string, projectDir: string, opts?: INpmInstallOptions): Promise<any> {
try {
const packageToInstall = this.$options.frameworkPath || packageName;
const pathToSave = projectDir;
const version = (opts && opts.version) || null;
const dependencyType = (opts && opts.dependencyType) || null;
16 changes: 4 additions & 12 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
@@ -144,7 +144,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
const targetSdkVersion = androidToolsInfo && androidToolsInfo.targetSdkVersion;
this.$logger.trace(`Using Android SDK '${targetSdkVersion}'.`);

this.isAndroidStudioTemplate = this.isAndroidStudioCompatibleTemplate(projectData);
this.isAndroidStudioTemplate = this.isAndroidStudioCompatibleTemplate(projectData, frameworkVersion);
if (this.isAndroidStudioTemplate) {
this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "*", "-R");
} else {
@@ -703,20 +703,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
}
}

private isAndroidStudioCompatibleTemplate(projectData: IProjectData): boolean {
private isAndroidStudioCompatibleTemplate(projectData: IProjectData, frameworkVersion?: string): boolean {
const currentPlatformData: IDictionary<any> = this.$projectDataService.getNSValue(projectData.projectDir, constants.TNS_ANDROID_RUNTIME_NAME);
let platformVersion = currentPlatformData && currentPlatformData[constants.VERSION_STRING];
const platformVersion = (currentPlatformData && currentPlatformData[constants.VERSION_STRING]) || frameworkVersion;

if (!platformVersion) {
const tnsAndroidPackageJsonPath = path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME, constants.TNS_ANDROID_RUNTIME_NAME, constants.PACKAGE_JSON_FILE_NAME);
if (this.$fs.exists(tnsAndroidPackageJsonPath)) {
const projectPackageJson: any = this.$fs.readJson(tnsAndroidPackageJsonPath);
if (projectPackageJson && projectPackageJson.version) {
platformVersion = projectPackageJson.version;
}
} else {
return true;
}
return true;
}

if (platformVersion === constants.PackageVersion.NEXT || platformVersion === constants.PackageVersion.LATEST || platformVersion === constants.PackageVersion.RC) {
39 changes: 18 additions & 21 deletions lib/services/platform-service.ts
Original file line number Diff line number Diff line change
@@ -36,11 +36,12 @@ export class PlatformService extends EventEmitter implements IPlatformService {
private $mobileHelper: Mobile.IMobileHelper,
private $hostInfo: IHostInfo,
private $devicePathProvider: IDevicePathProvider,
private $npm: INodePackageManager,
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
private $projectChangesService: IProjectChangesService,
private $analyticsService: IAnalyticsService,
private $terminalSpinnerService: ITerminalSpinnerService) {
private $terminalSpinnerService: ITerminalSpinnerService,
private $pacoteService: IPacoteService
) {
super();
}

@@ -92,10 +93,6 @@ export class PlatformService extends EventEmitter implements IPlatformService {

const platformData = this.$platformsData.getPlatformData(platform, projectData);

if (version === undefined) {
version = this.getCurrentPlatformVersion(platform, projectData);
}

// Log the values for project
this.$logger.trace("Creating NativeScript project for the %s platform", platform);
this.$logger.trace("Path: %s", platformData.projectRoot);
@@ -105,28 +102,28 @@ export class PlatformService extends EventEmitter implements IPlatformService {
this.$logger.out("Copying template files...");

let packageToInstall = "";
const npmOptions: IStringDictionary = {
pathToSave: path.join(projectData.platformsDir, platform),
dependencyType: "save"
};
if (frameworkPath) {
packageToInstall = path.resolve(frameworkPath);
} else {
if (!version) {
version = this.getCurrentPlatformVersion(platform, projectData) ||
await this.$npmInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName);
}

if (!frameworkPath) {
packageToInstall = platformData.frameworkPackageName;
npmOptions["version"] = version;
packageToInstall = `${platformData.frameworkPackageName}@${version}`;
}

const spinner = this.$terminalSpinnerService.createSpinner();
const projectDir = projectData.projectDir;
const platformPath = path.join(projectData.platformsDir, platform);

try {
spinner.start();
const downloadedPackagePath = await this.$npmInstallationManager.install(packageToInstall, projectDir, npmOptions);
const downloadedPackagePath = temp.mkdirSync("runtimeDir");
temp.track();
await this.$pacoteService.extractPackage(packageToInstall, downloadedPackagePath);
let frameworkDir = path.join(downloadedPackagePath, constants.PROJECT_FRAMEWORK_FOLDER_NAME);
frameworkDir = path.resolve(frameworkDir);

const coreModuleName = await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, config, nativePrepare);
await this.$npm.uninstall(coreModuleName, { save: true }, projectData.projectDir);
await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, config, nativePrepare);
} catch (err) {
this.$fs.deleteDirectory(platformPath);
throw err;
@@ -135,7 +132,7 @@ export class PlatformService extends EventEmitter implements IPlatformService {
}

this.$fs.ensureDirectoryExists(platformPath);
this.$logger.out("Project successfully created.");
this.$logger.out(`Platform ${platform} successfully added.`);
}

private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, nativePrepare?: INativePrepare): Promise<string> {
@@ -842,15 +839,15 @@ export class PlatformService extends EventEmitter implements IPlatformService {
const data = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName);
const currentVersion = data && data.version ? data.version : "0.2.0";

const installedModuleDir = temp.mkdirSync("runtime-to-update");
let newVersion = version === constants.PackageVersion.NEXT ?
await this.$npmInstallationManager.getNextVersion(platformData.frameworkPackageName) :
version || await this.$npmInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName);
const installedModuleDir = await this.$npmInstallationManager.install(platformData.frameworkPackageName, projectData.projectDir, { version: newVersion, dependencyType: "save" });
await this.$pacoteService.extractPackage(`${platformData.frameworkPackageName}@${newVersion}`, installedModuleDir);
const cachedPackageData = this.$fs.readJson(path.join(installedModuleDir, "package.json"));
newVersion = (cachedPackageData && cachedPackageData.version) || newVersion;

const canUpdate = platformData.platformProjectService.canUpdatePlatform(installedModuleDir, projectData);
await this.$npm.uninstall(platformData.frameworkPackageName, { save: true }, projectData.projectDir);
if (canUpdate) {
if (!semver.valid(newVersion)) {
this.$errors.fail("The version %s is not valid. The version should consists from 3 parts separated by dot.", newVersion);
3 changes: 3 additions & 0 deletions test/npm-support.ts
Original file line number Diff line number Diff line change
@@ -104,6 +104,9 @@ function createTestInjector(): IInjector {
getChanges: () => Promise.resolve({}),
generateHashes: () => Promise.resolve()
});
testInjector.register("pacoteService", {
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
});

return testInjector;
}
3 changes: 3 additions & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
@@ -168,6 +168,9 @@ function createTestInjector() {
testInjector.register("platformEnvironmentRequirements", {
checkEnvironmentRequirements: async (platform?: string, projectDir?: string, runtimeVersion?: string): Promise<boolean> => true
});
testInjector.register("pacoteService", {
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
});

return testInjector;
}
159 changes: 75 additions & 84 deletions test/platform-service.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import * as yok from "../lib/common/yok";
import * as stubs from "./stubs";
import * as PlatformServiceLib from "../lib/services/platform-service";
import * as StaticConfigLib from "../lib/config";
import { VERSION_STRING } from "../lib/constants";
import { VERSION_STRING, PACKAGE_JSON_FILE_NAME } from "../lib/constants";
import * as fsLib from "../lib/common/file-system";
import * as optionsLib from "../lib/options";
import * as hostInfoLib from "../lib/common/host-info";
@@ -23,6 +23,7 @@ import ProjectChangesLib = require("../lib/services/project-changes-service");
import { Messages } from "../lib/common/messages/messages";
import { SettingsService } from "../lib/common/test/unit-tests/stubs";
import { INFO_PLIST_FILE_NAME, MANIFEST_FILE_NAME } from "../lib/constants";
import { mkdir } from "shelljs";

require("should");
const temp = require("temp");
@@ -36,6 +37,7 @@ function createTestInjector() {
testInjector.register('logger', stubs.LoggerStub);
testInjector.register("nodeModulesDependenciesBuilder", {});
testInjector.register('npmInstallationManager', stubs.NpmInstallationManagerStub);
// TODO: Remove the projectData - it shouldn't be required in the service itself.
testInjector.register('projectData', stubs.ProjectDataStub);
testInjector.register('platformsData', stubs.PlatformsDataStub);
testInjector.register('devicesService', {});
@@ -108,7 +110,16 @@ function createTestInjector() {
testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub);
testInjector.register("filesHashService", {
generateHashes: () => Promise.resolve(),
getChanges: () => Promise.resolve({test: "testHash"})
getChanges: () => Promise.resolve({ test: "testHash" })
});
testInjector.register("pacoteService", {
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => {
mkdir(path.join(destinationDirectory, "framework"));
(new fsLib.FileSystem(testInjector)).writeFile(path.join(destinationDirectory, PACKAGE_JSON_FILE_NAME), JSON.stringify({
name: "package-name",
version: "1.0.0"
}));
}
});

return testInjector;
@@ -199,105 +210,85 @@ describe('Platform Service Tests', () => {
await platformService.addPlatforms(["IoS"], "", projectData, config);
await platformService.addPlatforms(["iOs"], "", projectData, config);
});

it("should fail if platform is already installed", async () => {
const projectData: IProjectData = testInjector.resolve("projectData");
// By default fs.exists returns true, so the platforms directory should exists
await assert.isRejected(platformService.addPlatforms(["android"], "", projectData, config));
await assert.isRejected(platformService.addPlatforms(["ios"], "", projectData, config));
await assert.isRejected(platformService.addPlatforms(["android"], "", projectData, config), "Platform android already added");
await assert.isRejected(platformService.addPlatforms(["ios"], "", projectData, config), "Platform ios already added");
});
it("should fail if npm is unavalible", async () => {
const fs = testInjector.resolve("fs");
fs.exists = () => false;

const errorMessage = "Npm is unavalible";
const npmInstallationManager = testInjector.resolve("npmInstallationManager");
npmInstallationManager.install = () => { throw new Error(errorMessage); };
const projectData: IProjectData = testInjector.resolve("projectData");

try {
await platformService.addPlatforms(["android"], "", projectData, config);
} catch (err) {
assert.equal(errorMessage, err.message);
}
});
it("should respect platform version in package.json's nativescript key", async () => {
const versionString = "2.5.0";
it("should fail if unable to extract runtime package", async () => {
const fs = testInjector.resolve("fs");
fs.exists = () => false;

const nsValueObject: any = {};
nsValueObject[VERSION_STRING] = versionString;
const projectDataService = testInjector.resolve("projectDataService");
projectDataService.getNSValue = () => nsValueObject;

const npmInstallationManager = testInjector.resolve("npmInstallationManager");
npmInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => {
assert.deepEqual(options.version, versionString);
return "";
const pacoteService = testInjector.resolve<IPacoteService>("pacoteService");
const errorMessage = "Pacote service unable to extract package";
pacoteService.extractPackage = async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => {
throw new Error(errorMessage);
};

const projectData: IProjectData = testInjector.resolve("projectData");

await platformService.addPlatforms(["android"], "", projectData, config);
await platformService.addPlatforms(["ios"], "", projectData, config);
await assert.isRejected(platformService.addPlatforms(["android"], "", projectData, config), errorMessage);
});
it("should install latest platform if no information found in package.json's nativescript key", async () => {

const assertCorrectDataIsPassedToPacoteService = async (versionString: string): Promise<void> => {
const fs = testInjector.resolve("fs");
fs.exists = () => false;

const projectDataService = testInjector.resolve("projectDataService");
projectDataService.getNSValue = (): any => null;

const npmInstallationManager = testInjector.resolve("npmInstallationManager");
npmInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => {
assert.deepEqual(options.version, undefined);
return "";
const pacoteService = testInjector.resolve<IPacoteService>("pacoteService");
let packageNamePassedToPacoteService = "";
pacoteService.extractPackage = async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => {
packageNamePassedToPacoteService = packageName;
};

const platformsData = testInjector.resolve<IPlatformsData>("platformsData");
const packageName = "packageName";
platformsData.getPlatformData = (platform: string, projectData: IProjectData): IPlatformData => {
return {
frameworkPackageName: packageName,
platformProjectService: new stubs.PlatformProjectServiceStub(),
emulatorServices: undefined,
projectRoot: "",
normalizedPlatformName: "",
appDestinationDirectoryPath: "",
deviceBuildOutputPath: "",
getValidBuildOutputData: (buildOptions: IBuildOutputOptions) => ({ packageNames: [] }),
frameworkFilesExtensions: [],
relativeToFrameworkConfigurationFilePath: "",
fastLivesyncFileExtensions: []
};
};
const projectData: IProjectData = testInjector.resolve("projectData");

await platformService.addPlatforms(["android"], "", projectData, config);
assert.equal(packageNamePassedToPacoteService, `${packageName}@${versionString}`);
await platformService.addPlatforms(["ios"], "", projectData, config);
});
});
describe("#add platform(ios)", () => {
it("should call validate method", async () => {
const fs = testInjector.resolve("fs");
fs.exists = () => false;

const errorMessage = "Xcode is not installed or Xcode version is smaller that 5.0";
const platformsData = testInjector.resolve("platformsData");
const platformProjectService = platformsData.getPlatformData().platformProjectService;
const projectData: IProjectData = testInjector.resolve("projectData");
platformProjectService.validate = () => {
throw new Error(errorMessage);
assert.equal(packageNamePassedToPacoteService, `${packageName}@${versionString}`);
};
it("should respect platform version in package.json's nativescript key", async () => {
const versionString = "2.5.0";
const nsValueObject: any = {
[VERSION_STRING]: versionString
};
const projectDataService = testInjector.resolve("projectDataService");
projectDataService.getNSValue = () => nsValueObject;

try {
await platformService.addPlatforms(["ios"], "", projectData, config);
} catch (err) {
assert.equal(errorMessage, err.message);
}
await assertCorrectDataIsPassedToPacoteService(versionString);
});
});
describe("#add platform(android)", () => {
it("should fail if java, ant or android are not installed", async () => {
const fs = testInjector.resolve("fs");
fs.exists = () => false;

const errorMessage = "Java, ant or android are not installed";
const platformsData = testInjector.resolve("platformsData");
const platformProjectService = platformsData.getPlatformData().platformProjectService;
platformProjectService.validate = () => {
throw new Error(errorMessage);
it("should install latest platform if no information found in package.json's nativescript key", async () => {

const projectDataService = testInjector.resolve("projectDataService");
projectDataService.getNSValue = (): any => null;

const latestCompatibleVersion = "1.0.0";
const npmInstallationManager = testInjector.resolve<INpmInstallationManager>("npmInstallationManager");
npmInstallationManager.getLatestCompatibleVersion = async (packageName: string, referenceVersion?: string): Promise<string> => {
return latestCompatibleVersion;
};
const projectData: IProjectData = testInjector.resolve("projectData");

try {
await platformService.addPlatforms(["android"], "", projectData, config);
} catch (err) {
assert.equal(errorMessage, err.message);
}
await assertCorrectDataIsPassedToPacoteService(latestCompatibleVersion);
});
});
});
@@ -962,18 +953,18 @@ describe('Platform Service Tests', () => {
}, {
name: "productFlavors are specified in .gradle file",
buildOutput: [`/my/path/arm64Demo/${configuration}/${apkName}-arm64-demo-${configuration}.apk`,
`/my/path/arm64Full/${configuration}/${apkName}-arm64-full-${configuration}.apk`,
`/my/path/armDemo/${configuration}/${apkName}-arm-demo-${configuration}.apk`,
`/my/path/armFull/${configuration}/${apkName}-arm-full-${configuration}.apk`,
`/my/path/x86Demo/${configuration}/${apkName}-x86-demo-${configuration}.apk`,
`/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`],
`/my/path/arm64Full/${configuration}/${apkName}-arm64-full-${configuration}.apk`,
`/my/path/armDemo/${configuration}/${apkName}-arm-demo-${configuration}.apk`,
`/my/path/armFull/${configuration}/${apkName}-arm-full-${configuration}.apk`,
`/my/path/x86Demo/${configuration}/${apkName}-x86-demo-${configuration}.apk`,
`/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`],
expectedResult: `/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`
}, {
name: "split options are specified in .gradle file",
buildOutput: [`/my/path/${configuration}/${apkName}-arm64-v8a-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-armeabi-v7a-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-universal-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-x86-${configuration}.apk`],
`/my/path/${configuration}/${apkName}-armeabi-v7a-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-universal-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-x86-${configuration}.apk`],
expectedResult: `/my/path/${configuration}/${apkName}-x86-${configuration}.apk`
}, {
name: "android-runtime has version < 4.0.0",
@@ -983,7 +974,7 @@ describe('Platform Service Tests', () => {
}

const platform = "Android";
const buildConfigs = [{buildForDevice: false}, {buildForDevice: true}];
const buildConfigs = [{ buildForDevice: false }, { buildForDevice: true }];
const apkNames = ["app", "testProj"];
const configurations = ["debug", "release"];

@@ -993,7 +984,7 @@ describe('Platform Service Tests', () => {
_.each(getTestCases(configuration, apkName), testCase => {
it(`should find correct ${configuration} ${apkName}.apk when ${testCase.name} and buildConfig is ${JSON.stringify(buildConfig)}`, async () => {
mockData(testCase.buildOutput, apkName);
const actualResult = await platformService.buildPlatform(platform, <IBuildConfig>buildConfig, <IProjectData>{projectName: ""});
const actualResult = await platformService.buildPlatform(platform, <IBuildConfig>buildConfig, <IProjectData>{ projectName: "" });
assert.deepEqual(actualResult, testCase.expectedResult);
});
});