Skip to content

Commit 6b6c5fa

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
1 parent 97d72e9 commit 6b6c5fa

15 files changed

+262
-71
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

ios/testApp/testApp.entitlements

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>aps-environment</key>
6+
<string>production</string>
7+
<key>nameKey</key>
8+
<string>appResources</string>
9+
</dict>
10+
</plist>

lib/bootstrap.ts

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

105+
$injector.require("androidResourcesMigrationService", "./services/android-resources-migration-service");
106+
$injector.requireCommand("resources-update", "./commands/resources-update");
107+
105108
$injector.require("androidToolsInfo", "./android-tools-info");
106109
$injector.require("devicePathProvider", "./device-path-provider");
107110

lib/commands/resources-update.ts

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: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ 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";
77
import { attachAwaitDetach } from "../common/helpers";
8-
import { EOL } from "os";
98
import { Configurations } from "../common/constants";
109
import { SpawnOptions } from "child_process";
1110

@@ -36,7 +35,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
3635
private $injector: IInjector,
3736
private $pluginVariablesService: IPluginVariablesService,
3837
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
39-
private $npm: INodePackageManager) {
38+
private $npm: INodePackageManager,
39+
private $androidResourcesMigrationService: IAndroidResourcesMigrationService) {
4040
super($fs, $projectDataService);
4141
this._androidProjectPropertiesManagers = Object.create(null);
4242
this.isAndroidStudioTemplate = false;
@@ -133,18 +133,14 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
133133
return Promise.resolve(true);
134134
}
135135

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

139+
if (appResourcesDirStructureHasMigrated) {
140+
return this.getUpdatedAppResourcesDestinationDirPath(projectData);
141+
} else {
142+
return this.getLegacyAppResourcesDestinationDirPath(projectData);
145143
}
146-
147-
return path.join(this.getPlatformData(projectData).projectRoot, constants.RESOURCES_DIR);
148144
}
149145

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

202-
this.cleanResValues(targetSdkVersion, projectData, frameworkVersion);
198+
this.cleanResValues(targetSdkVersion, projectData);
203199

204200
const npmConfig: INodePackageManagerInstallOptions = {
205201
save: true,
@@ -235,8 +231,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
235231
}
236232
}
237233

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

264-
const stringsFilePath = path.join(this.getAppResourcesDestinationDirectoryPath(projectData), 'values', 'strings.xml');
265269
shell.sed('-i', /__NAME__/, projectData.projectName, stringsFilePath);
266270
shell.sed('-i', /__TITLE_ACTIVITY__/, projectData.projectName, stringsFilePath);
267271

@@ -317,33 +321,28 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
317321
}
318322

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

349348
private getBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array<string> {
@@ -391,7 +390,15 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
391390
}
392391

393392
public ensureConfigurationFileInAppResources(projectData: IProjectData): void {
394-
const originalAndroidManifestFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName);
393+
const appResourcesDirectoryPath = projectData.appResourcesDirectoryPath;
394+
const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(appResourcesDirectoryPath);
395+
let originalAndroidManifestFilePath;
396+
397+
if (appResourcesDirStructureHasMigrated) {
398+
originalAndroidManifestFilePath = path.join(appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, "src", "main", this.getPlatformData(projectData).configurationFileName);
399+
} else {
400+
originalAndroidManifestFilePath = path.join(appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName);
401+
}
395402

396403
const manifestExists = this.$fs.exists(originalAndroidManifestFilePath);
397404

@@ -400,16 +407,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
400407
return;
401408
}
402409
// Overwrite the AndroidManifest from runtime.
403-
this.$fs.copyFile(originalAndroidManifestFilePath, this.getPlatformData(projectData).configurationFilePath);
410+
if (!appResourcesDirStructureHasMigrated) {
411+
this.$fs.copyFile(originalAndroidManifestFilePath, this.getPlatformData(projectData).configurationFilePath);
412+
}
404413
}
405414

406415
public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void {
407-
const resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName);
408-
const valuesDirRegExp = /^values/;
409-
const resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp));
410-
_.each(resourcesDirs, resourceDir => {
411-
this.$fs.deleteDirectory(path.join(this.getAppResourcesDestinationDirectoryPath(projectData), resourceDir));
412-
});
416+
// Intentionally left empty
413417
}
414418

415419
public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise<void> {
@@ -559,20 +563,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
559563
// Nothing android specific to check yet.
560564
}
561565

562-
private _canUseGradle: boolean;
563-
private canUseGradle(projectData: IProjectData, frameworkVersion?: string): boolean {
564-
if (!this._canUseGradle) {
565-
if (!frameworkVersion) {
566-
const frameworkInfoInProjectFile = this.$projectDataService.getNSValue(projectData.projectDir, this.getPlatformData(projectData).frameworkPackageName);
567-
frameworkVersion = frameworkInfoInProjectFile && frameworkInfoInProjectFile.version;
568-
}
569-
570-
this._canUseGradle = !frameworkVersion || semver.gte(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE);
571-
}
572-
573-
return this._canUseGradle;
574-
}
575-
576566
private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): void {
577567
const paths = files.split(' ').map(p => path.join(frameworkDir, p));
578568
shell.cp(cpArg, paths, projectRoot);
@@ -676,6 +666,24 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
676666

677667
return semver.gte(normalizedPlatformVersion, androidStudioCompatibleTemplate);
678668
}
669+
670+
private getLegacyAppResourcesDestinationDirPath(projectData: IProjectData): string {
671+
const resourcePath: string[] = [constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR];
672+
if (this.isAndroidStudioTemplate) {
673+
resourcePath.unshift(constants.APP_FOLDER_NAME);
674+
}
675+
676+
return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath);
677+
}
678+
679+
private getUpdatedAppResourcesDestinationDirPath(projectData: IProjectData): string {
680+
const resourcePath: string[] = [constants.SRC_DIR];
681+
if (this.isAndroidStudioTemplate) {
682+
resourcePath.unshift(constants.APP_FOLDER_NAME);
683+
}
684+
685+
return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath);
686+
}
679687
}
680688

681689
$injector.register("androidProjectService", AndroidProjectService);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as path from "path";
2+
import * as shell from "shelljs";
3+
import * as constants from "../constants";
4+
5+
export class AndroidResourcesMigrationService implements IAndroidResourcesMigrationService {
6+
private static ANDROID_DIR = "Android";
7+
private static ANDROID_DIR_TEMP = "Android-Updated";
8+
private static ANDROID_DIR_OLD = "Android-Pre-v4";
9+
10+
constructor(private $fs: IFileSystem,
11+
private $logger: ILogger,
12+
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { }
13+
14+
public canMigrate(platformString: string): boolean {
15+
return platformString.toLowerCase() === this.$devicePlatformsConstants.Android.toLowerCase();
16+
}
17+
18+
public hasMigrated(appResourcesDir: string): boolean {
19+
return this.$fs.exists(path.join(appResourcesDir, AndroidResourcesMigrationService.ANDROID_DIR, constants.SRC_DIR, constants.MAIN_DIR));
20+
}
21+
22+
public async migrate(appResourcesDir: string): Promise<void> {
23+
const originalAppResources = path.join(appResourcesDir, AndroidResourcesMigrationService.ANDROID_DIR);
24+
const appResourcesDestination = path.join(appResourcesDir, AndroidResourcesMigrationService.ANDROID_DIR_TEMP);
25+
const appMainSourceSet = path.join(appResourcesDestination, constants.SRC_DIR, constants.MAIN_DIR);
26+
const appResourcesMainSourceSetResourcesDestination = path.join(appMainSourceSet, constants.RESOURCES_DIR);
27+
28+
this.$fs.ensureDirectoryExists(appResourcesDestination);
29+
this.$fs.ensureDirectoryExists(appMainSourceSet);
30+
// create /java, /res and /assets in the App_Resources/Android/src/main directory
31+
this.$fs.ensureDirectoryExists(appResourcesMainSourceSetResourcesDestination);
32+
this.$fs.ensureDirectoryExists(path.join(appMainSourceSet, "java"));
33+
this.$fs.ensureDirectoryExists(path.join(appMainSourceSet, constants.ASSETS_DIR));
34+
35+
const isDirectory = (source: string) => this.$fs.getLsStats(source).isDirectory();
36+
const getDirectories = (source: string) =>
37+
this.$fs.readDirectory(source).map(name => path.join(source, name)).filter(isDirectory);
38+
39+
this.$fs.copyFile(path.join(originalAppResources, "app.gradle"), path.join(appResourcesDestination, "app.gradle"));
40+
this.$fs.copyFile(path.join(originalAppResources, constants.MANIFEST_FILE_NAME), path.join(appMainSourceSet, constants.MANIFEST_FILE_NAME));
41+
42+
const resourceDirectories = getDirectories(originalAppResources);
43+
44+
resourceDirectories.forEach(dir => {
45+
this.$fs.copyFile(dir, appResourcesMainSourceSetResourcesDestination);
46+
});
47+
48+
// rename the legacy app_resources to ANDROID_DIR_OLD
49+
shell.mv(originalAppResources, path.join(appResourcesDir, AndroidResourcesMigrationService.ANDROID_DIR_OLD));
50+
// move the new, updated app_resources to App_Resources/Android, as the de facto resources
51+
shell.mv(appResourcesDestination, originalAppResources);
52+
53+
this.$logger.out(`Successfully updated your project's application resources '/Android' directory structure.\nThe previous version of your Android application resources has been renamed to '/${AndroidResourcesMigrationService.ANDROID_DIR_OLD}'`);
54+
}
55+
}
56+
57+
$injector.register("androidResourcesMigrationService", AndroidResourcesMigrationService);

0 commit comments

Comments
 (0)