Skip to content

Commit a5d89f4

Browse files
committed
feat(resources-update-command): introduce resources-update command
Introduce the 'tns resources-update android' command. By design, upon execution it should migrate the directory structure of App_Resources/Android to the new v4 structure - the one that supports inclusion of java source files, arbitrary assets, and any resource files in the App_Resources/Android/src/main directory structure. Additional, user-defined flavors can also be created taking advantage of the new dir structure. docs(resources-update-command): add documentation for the new resources-update command fix(resources-update-command): make prepare and run backward-compatible fix(resource-update-command-tests): inject the new service in tests chore: address PR comments chore: fix git rebase error
1 parent 283b31c commit a5d89f4

14 files changed

+289
-73
lines changed

docs/man_pages/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Command | Description
3232
[platform list](project/configuration/platform.html) | Lists all platforms that the project currently targets.
3333
[platform&nbsp;remove&nbsp;`<Platform>`](project/configuration/platform-remove.html) | Removes the selected platform from the platforms that the project currently targets. This operation deletes all platform-specific files and subdirectories from your project.
3434
[platform update `<Platform>`](project/configuration/platform-update.html) | Updates the NativeScript runtime for the specified platform.
35+
[resources-update](project/configuration/resources-update.html) | Updates the App_Resources/<platform>'s internal folder structure to conform to that of an Android project.
3536
[prepare `<Platform>`](project/configuration/prepare.html) | Copies relevant content from the app directory to the subdirectory for the selected target platform to let you build the project.
3637
[build `<Platform>`](project/testing/build.html) | Builds the project for the selected target platform and produces an application package or an emulator package.
3738
[deploy `<Platform>`](project/testing/deploy.html) | Deploys the project to a connected physical or virtual device.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<% if (isJekyll) { %>---
2+
title: tns resources-update
3+
position: 9
4+
---<% } %>
5+
#tns resources-update
6+
==========
7+
8+
Usage | Synopsis
9+
------|-------
10+
`$ tns resources-update` | Defaults to executing `$ tns resources-update android`. Updates the App_Resources/Android's folder structure.
11+
`$ tns resources-update android` | Updates the App_Resources/Android's folder structure.
12+
13+
Updates the App_Resources/<platform>'s internal folder structure to conform to that of an Android project. Android resource files and directories will be located at the following paths:
14+
- `drawable-*`, `values`, `raw`, etc. can be found at `App_Resources/Android/src/main/res`
15+
- `AndroidManifest.xml` can be found at `App_Resources/Android/src/main/AndroidManifest.xml`
16+
- Java source files can be dropped in at `App_Resources/Android/src/main/java` after creating the proper package subdirectory structure
17+
- Additional arbitrary assets can be dropped in at `App_Resources/Android/src/main/assets`
18+
19+
### Command Limitations
20+
21+
* The command works only for the directory structure under `App_Resources/Android`. Running `$ tns resources-update ios` will have no effect.
22+
23+
### Related Commands
24+
25+
Command | Description
26+
----------|----------
27+
[install](install.html) | Installs all platforms and dependencies described in the `package.json` file in the current directory.
28+
[platform add](platform-add.html) | Configures the current project to target the selected platform.
29+
[platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets.
30+
[platform](platform.html) | Lists all platforms that the project currently targets.
31+
[prepare](prepare.html) | Copies common and relevant platform-specific content from the app directory to the subdirectory for the selected target platform in the platforms directory.

docs/man_pages/project/configuration/update.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ Command | Description
2121
[platform remove](platform-remove.html) | Removes the selected platform from the platforms that the project currently targets.
2222
[platform](platform.html) | Lists all platforms that the project currently targets.
2323
[prepare](prepare.html) | Copies common and relevant platform-specific content from the app directory to the subdirectory for the selected target platform in the platforms directory.
24-
[platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform.
24+
[platform update](platform-update.html) | Updates the NativeScript runtime for the specified platform.
25+
[resources-update android](resources-update.html) | Updates the App_Resources/Android directory to the new v4.0 directory structure

lib/bootstrap.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ $injector.requireCommand("init", "./commands/init");
104104
$injector.require("infoService", "./services/info-service");
105105
$injector.requireCommand("info", "./commands/info");
106106

107+
$injector.require("androidResourcesMigrationService", "./services/android-resources-migration-service");
108+
$injector.requireCommand("resources|update", "./commands/resources/resources-update");
109+
107110
$injector.require("androidToolsInfo", "./android-tools-info");
108111
$injector.require("devicePathProvider", "./device-path-provider");
109112

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export class ResourcesUpdateCommand implements ICommand {
2+
public allowedParameters: ICommandParameter[] = [];
3+
4+
constructor(private $projectData: IProjectData,
5+
private $errors: IErrors,
6+
private $androidResourcesMigrationService: IAndroidResourcesMigrationService) {
7+
this.$projectData.initializeProjectData();
8+
}
9+
10+
public async execute(args: string[]): Promise<void> {
11+
await this.$androidResourcesMigrationService.migrate(this.$projectData.getAppResourcesDirectoryPath());
12+
}
13+
14+
public async canExecute(args: string[]): Promise<boolean> {
15+
if (!args || args.length === 0) {
16+
// Command defaults to migrating the Android App_Resources, unless explicitly specified
17+
args = ["android"];
18+
}
19+
20+
for (const platform of args) {
21+
if (!this.$androidResourcesMigrationService.canMigrate(platform)) {
22+
this.$errors.failWithoutHelp(`The ${platform} does not need to have its resources updated.`);
23+
}
24+
25+
if (this.$androidResourcesMigrationService.hasMigrated(this.$projectData.getAppResourcesDirectoryPath())) {
26+
this.$errors.failWithoutHelp("The App_Resources have already been updated for the Android platform.");
27+
}
28+
}
29+
30+
return true;
31+
}
32+
}
33+
34+
$injector.registerCommand("resources|update", ResourcesUpdateCommand);

lib/declarations.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,12 @@ interface IInfoService {
497497
printComponentsInfo(): Promise<void>;
498498
}
499499

500+
interface IAndroidResourcesMigrationService {
501+
canMigrate(platformString: string): boolean;
502+
hasMigrated(appResourcesDir: string): boolean;
503+
migrate(appResourcesDir: string): Promise<void>;
504+
}
505+
500506
/**
501507
* Describes properties needed for uploading a package to iTunes Connect
502508
*/

lib/services/android-project-service.ts

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as constants from "../constants";
44
import * as semver from "semver";
55
import * as projectServiceBaseLib from "./platform-project-service-base";
66
import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-android-debug-bridge";
7-
import { EOL } from "os";
87
import { attachAwaitDetach, isRecommendedAarFile } from "../common/helpers";
98
import { Configurations } from "../common/constants";
109
import { SpawnOptions } from "child_process";
@@ -37,7 +36,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
3736
private $pluginVariablesService: IPluginVariablesService,
3837
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
3938
private $npm: INodePackageManager,
40-
private $androidPluginBuildService: IAndroidPluginBuildService) {
39+
private $androidPluginBuildService: IAndroidPluginBuildService,
40+
private $androidResourcesMigrationService: IAndroidResourcesMigrationService) {
4141
super($fs, $projectDataService);
4242
this._androidProjectPropertiesManagers = Object.create(null);
4343
this.isAndroidStudioTemplate = false;
@@ -134,18 +134,14 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
134134
return Promise.resolve(true);
135135
}
136136

137-
public getAppResourcesDestinationDirectoryPath(projectData: IProjectData, frameworkVersion?: string): string {
138-
if (this.canUseGradle(projectData, frameworkVersion)) {
139-
const resourcePath: string[] = [constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR];
140-
if (this.isAndroidStudioTemplate) {
141-
resourcePath.unshift(constants.APP_FOLDER_NAME);
142-
}
143-
144-
return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath);
137+
public getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string {
138+
const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(projectData.getAppResourcesDirectoryPath());
145139

140+
if (appResourcesDirStructureHasMigrated) {
141+
return this.getUpdatedAppResourcesDestinationDirPath(projectData);
142+
} else {
143+
return this.getLegacyAppResourcesDestinationDirPath(projectData);
146144
}
147-
148-
return path.join(this.getPlatformData(projectData).projectRoot, constants.RESOURCES_DIR);
149145
}
150146

151147
public async validate(projectData: IProjectData): Promise<void> {
@@ -200,7 +196,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
200196
this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "gradlew gradlew.bat", "-f");
201197
}
202198

203-
this.cleanResValues(targetSdkVersion, projectData, frameworkVersion);
199+
this.cleanResValues(targetSdkVersion, projectData);
204200

205201
const npmConfig: INodePackageManagerInstallOptions = {
206202
save: true,
@@ -236,8 +232,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
236232
}
237233
}
238234

239-
private cleanResValues(targetSdkVersion: number, projectData: IProjectData, frameworkVersion: string): void {
240-
const resDestinationDir = this.getAppResourcesDestinationDirectoryPath(projectData, frameworkVersion);
235+
private cleanResValues(targetSdkVersion: number, projectData: IProjectData): void {
236+
const resDestinationDir = this.getAppResourcesDestinationDirectoryPath(projectData);
241237
const directoriesInResFolder = this.$fs.readDirectory(resDestinationDir);
242238
const directoriesToClean = directoriesInResFolder
243239
.map(dir => {
@@ -261,8 +257,17 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
261257
public async interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise<void> {
262258
// Interpolate the apilevel and package
263259
this.interpolateConfigurationFile(projectData, platformSpecificData);
260+
const appResourcesDirectoryPath = projectData.getAppResourcesDirectoryPath();
261+
262+
let stringsFilePath: string;
263+
264+
const appResourcesDestinationDirectoryPath = this.getAppResourcesDestinationDirectoryPath(projectData);
265+
if (this.$androidResourcesMigrationService.hasMigrated(appResourcesDirectoryPath)) {
266+
stringsFilePath = path.join(appResourcesDestinationDirectoryPath, constants.MAIN_DIR, constants.RESOURCES_DIR, 'values', 'strings.xml');
267+
} else {
268+
stringsFilePath = path.join(appResourcesDestinationDirectoryPath, 'values', 'strings.xml');
269+
}
264270

265-
const stringsFilePath = path.join(this.getAppResourcesDestinationDirectoryPath(projectData), 'values', 'strings.xml');
266271
shell.sed('-i', /__NAME__/, projectData.projectName, stringsFilePath);
267272
shell.sed('-i', /__TITLE_ACTIVITY__/, projectData.projectName, stringsFilePath);
268273

@@ -318,33 +323,28 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
318323
}
319324

320325
public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
321-
if (this.canUseGradle(projectData)) {
322-
const buildOptions = this.getGradleBuildOptions(buildConfig, projectData);
323-
if (this.$logger.getLevel() === "TRACE") {
324-
buildOptions.unshift("--stacktrace");
325-
buildOptions.unshift("--debug");
326-
}
327-
if (buildConfig.release) {
328-
buildOptions.unshift("assembleRelease");
329-
} else {
330-
buildOptions.unshift("assembleDebug");
331-
}
332-
333-
const handler = (data: any) => {
334-
this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data);
335-
};
336-
337-
await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME,
338-
this.$childProcess,
339-
handler,
340-
this.executeCommand(this.getPlatformData(projectData).projectRoot,
341-
buildOptions,
342-
{ stdio: buildConfig.buildOutputStdio || "inherit" },
343-
{ emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }));
326+
const buildOptions = this.getGradleBuildOptions(buildConfig, projectData);
327+
if (this.$logger.getLevel() === "TRACE") {
328+
buildOptions.unshift("--stacktrace");
329+
buildOptions.unshift("--debug");
330+
}
331+
if (buildConfig.release) {
332+
buildOptions.unshift("assembleRelease");
344333
} else {
345-
this.$errors.failWithoutHelp("Cannot complete build because this project is ANT-based." + EOL +
346-
"Run `tns platform remove android && tns platform add android` to switch to Gradle and try again.");
334+
buildOptions.unshift("assembleDebug");
347335
}
336+
337+
const handler = (data: any) => {
338+
this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data);
339+
};
340+
341+
await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME,
342+
this.$childProcess,
343+
handler,
344+
this.executeCommand(this.getPlatformData(projectData).projectRoot,
345+
buildOptions,
346+
{ stdio: buildConfig.buildOutputStdio || "inherit" },
347+
{ emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }));
348348
}
349349

350350
private getGradleBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array<string> {
@@ -400,7 +400,15 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
400400
}
401401

402402
public ensureConfigurationFileInAppResources(projectData: IProjectData): void {
403-
const originalAndroidManifestFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName);
403+
const appResourcesDirectoryPath = projectData.appResourcesDirectoryPath;
404+
const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(appResourcesDirectoryPath);
405+
let originalAndroidManifestFilePath;
406+
407+
if (appResourcesDirStructureHasMigrated) {
408+
originalAndroidManifestFilePath = path.join(appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, "src", "main", this.getPlatformData(projectData).configurationFileName);
409+
} else {
410+
originalAndroidManifestFilePath = path.join(appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName);
411+
}
404412

405413
const manifestExists = this.$fs.exists(originalAndroidManifestFilePath);
406414

@@ -409,16 +417,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
409417
return;
410418
}
411419
// Overwrite the AndroidManifest from runtime.
412-
this.$fs.copyFile(originalAndroidManifestFilePath, this.getPlatformData(projectData).configurationFilePath);
420+
if (!appResourcesDirStructureHasMigrated) {
421+
this.$fs.copyFile(originalAndroidManifestFilePath, this.getPlatformData(projectData).configurationFilePath);
422+
}
413423
}
414424

415425
public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void {
416-
const resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName);
417-
const valuesDirRegExp = /^values/;
418-
const resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp));
419-
_.each(resourcesDirs, resourceDir => {
420-
this.$fs.deleteDirectory(path.join(this.getAppResourcesDestinationDirectoryPath(projectData), resourceDir));
421-
});
426+
this.cleanUpPreparedResources(appResourcesDirectoryPath, projectData);
422427
}
423428

424429
public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise<void> {
@@ -643,20 +648,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
643648
// Nothing android specific to check yet.
644649
}
645650

646-
private _canUseGradle: boolean;
647-
private canUseGradle(projectData: IProjectData, frameworkVersion?: string): boolean {
648-
if (!this._canUseGradle) {
649-
if (!frameworkVersion) {
650-
const frameworkInfoInProjectFile = this.$projectDataService.getNSValue(projectData.projectDir, this.getPlatformData(projectData).frameworkPackageName);
651-
frameworkVersion = frameworkInfoInProjectFile && frameworkInfoInProjectFile.version;
652-
}
653-
654-
this._canUseGradle = !frameworkVersion || semver.gte(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE);
655-
}
656-
657-
return this._canUseGradle;
658-
}
659-
660651
private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): void {
661652
const paths = files.split(' ').map(p => path.join(frameworkDir, p));
662653
shell.cp(cpArg, paths, projectRoot);
@@ -761,6 +752,40 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
761752
const normalizedPlatformVersion = `${semver.major(platformVersion)}.${semver.minor(platformVersion)}.0`;
762753
return semver.gte(normalizedPlatformVersion, versionString);
763754
}
755+
756+
private getLegacyAppResourcesDestinationDirPath(projectData: IProjectData): string {
757+
const resourcePath: string[] = [constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR];
758+
if (this.isAndroidStudioTemplate) {
759+
resourcePath.unshift(constants.APP_FOLDER_NAME);
760+
}
761+
762+
return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath);
763+
}
764+
765+
private getUpdatedAppResourcesDestinationDirPath(projectData: IProjectData): string {
766+
const resourcePath: string[] = [constants.SRC_DIR];
767+
if (this.isAndroidStudioTemplate) {
768+
resourcePath.unshift(constants.APP_FOLDER_NAME);
769+
}
770+
771+
return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath);
772+
}
773+
774+
private cleanUpPreparedResources(appResourcesDirectoryPath: string, projectData: IProjectData): void {
775+
let resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName);
776+
if (this.$androidResourcesMigrationService.hasMigrated(projectData.appResourcesDirectoryPath)) {
777+
resourcesDirPath = path.join(resourcesDirPath, constants.MAIN_DIR, constants.RESOURCES_DIR);
778+
}
779+
780+
const valuesDirRegExp = /^values/;
781+
if (this.$fs.exists(resourcesDirPath)) {
782+
const resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp));
783+
const appResourcesDestinationDirectoryPath = this.getAppResourcesDestinationDirectoryPath(projectData);
784+
_.each(resourcesDirs, resourceDir => {
785+
this.$fs.deleteDirectory(path.join(appResourcesDestinationDirectoryPath, resourceDir));
786+
});
787+
}
788+
}
764789
}
765790

766791
$injector.register("androidProjectService", AndroidProjectService);

0 commit comments

Comments
 (0)