diff --git a/.npmignore b/.npmignore index b724e00efa..4d66a58b90 100644 --- a/.npmignore +++ b/.npmignore @@ -31,4 +31,5 @@ scratch/ docs/html/ dev/ -.travis/**/* \ No newline at end of file +.travis/**/* +generate_changelog.js \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ae3f35d6..18aa3ae739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ NativeScript CLI Changelog ================ +6.2.2 (2019, November 22) +== + +### Fixed +* [Fixed #5126](https://github.com/NativeScript/nativescript-cli/issues/5126): CLI does not generate all icons + +6.2.1 (2019, November 18) +== + +### Fixed +* [Fixed #5120](https://github.com/NativeScript/nativescript-cli/issues/5120): Android resource directories are not prepared correctly +* [Fixed #5105](https://github.com/NativeScript/nativescript-cli/issues/5105): App restarts when changing platform specific scss + +6.2.0 (2019, November 1) +== + +### New +* [Implemented #5038](https://github.com/NativeScript/nativescript-cli/issues/5038): Deprecate support for markingMode:full +* [Implemented #5049](https://github.com/NativeScript/nativescript-cli/issues/5049): Android App Bundle Improvements +* [Implemented #5060](https://github.com/NativeScript/nativescript-cli/issues/5060): Kotlin usage tracking in android builds +* [Implemented #5096](https://github.com/NativeScript/nativescript-cli/issues/5096): Reduce the npm requests when checking if the project should be migrated +* [Implemented #5104](https://github.com/NativeScript/nativescript-cli/pull/5104): Allow tag and range versions in the preview app plugin versions validation +* [Implemented #5107](https://github.com/NativeScript/nativescript-cli/issues/5107): Support V8 Snapshots on Windows + +### Fixed +* [Fixed #3785](https://github.com/NativeScript/nativescript-cli/issues/3785): NativeScript CLI doesn't pause on webpack compilation errors +* [Fixed #4681](https://github.com/NativeScript/nativescript-cli/issues/4681): `tns update ios` is not working +* [Fixed #4963](https://github.com/NativeScript/nativescript-cli/issues/4963): Difference in hookArgs.prepareData.platform on prepare and run command +* [Fixed #4995](https://github.com/NativeScript/nativescript-cli/issues/4995): Building plugin and running demo app fails if plugins has a surrounding gradle build +* [Fixed #5005](https://github.com/NativeScript/nativescript-cli/issues/5005): Apple Watch extension with space in the name of `.entitlements` file is not working +* [Fixed #5020](https://github.com/NativeScript/nativescript-cli/issues/5020): Stuck at "Restarting application on device" on Windows 10, iPad mini 2, compiled with NativeScript Sidekick cloud service. +* [Fixed #5030](https://github.com/NativeScript/nativescript-cli/issues/5030): The `tns devices` command lists appletv as iOS platform +* [Fixed #5034](https://github.com/NativeScript/nativescript-cli/issues/5034): Broken build when passing --i-cloud-container-environment +* [Fixed #5056](https://github.com/NativeScript/nativescript-cli/issues/5056): Unable to process native iOS files and frameworks from scoped packages +* [Fixed #5061](https://github.com/NativeScript/nativescript-cli/issues/5061): Unable to resolve cocoapods version conflicts +* [Fixed #5063](https://github.com/NativeScript/nativescript-cli/issues/5063): Splash Screen asset generation fails for iOS +* [Fixed #5070](https://github.com/NativeScript/nativescript-cli/issues/5070): The `tns test` command cannot work if the source code is not in `src` or `app` folder +* [Fixed #5077](https://github.com/NativeScript/nativescript-cli/pull/5077): Pass allowProvisioningUpdates to xcodebuild only when building for device +* [Fixed #5094](https://github.com/NativeScript/nativescript-cli/issues/5094): Add Theme v2 name to non-extenal modules when starting webpack 6.1.2 (2019, September 18) == diff --git a/generate_changelog.js b/generate_changelog.js new file mode 100644 index 0000000000..1b5f9588ba --- /dev/null +++ b/generate_changelog.js @@ -0,0 +1,195 @@ +"use strict"; + +const _ = require("lodash"); +const request = require("request"); +const fs = require("fs"); +const path = require("path"); +require("colors"); + +const argv = process.argv; +if (argv.length < 3 || argv.length > 4) { + console.error(`Incorrect usage. You need to pass the milestone and optionally the Authorization token.\n`.red + + `### Example: +node generate_changelog.js 6.2.2 2d2156c261bb1494f7a6e22f11fa446c7ca0e6b7\n`.yellow); + process.exit(127); +} + +const selectedMilestone = process.argv[2]; +const token = process.argv[3] || process.env.NS_CLI_CHANGELOG_AUTHORIZATION; +if (!token) { + console.error(`Unable to find Authorization token.\n`.red + + `You must either set NS_CLI_CHANGELOG_AUTHORIZATION environment variable or pass the token as an argument to the script:\n`.yellow + + `node generate_changelog.js 6.2.2 2d2156c261bb1494f7a6e22f11fa446c7ca0e6b7\n`.green); + process.exit(127); +} + +const sendRequest = (query) => { + return new Promise((resolve, reject) => { + request.post("https://api.github.com/graphql", { + headers: { + "Accept": "application/json", + "Authorization": `Bearer ${token}`, + "User-Agent": "NativeScript CLI Changelog Generator" + }, + body: JSON.stringify(query), + followAllRedirects: true + }, (err, response, body) => { + if (err) { + reject(err); + return; + } + resolve(JSON.parse(body)); + }); + }); +}; + +const getMilestonesInfoQuery = { + query: `{ + repository(owner:"NativeScript", name:"nativescript-cli") { + milestones(first: 100, states: OPEN) { + nodes { + number + id + title + url + } + } + } +}` +}; + +sendRequest(getMilestonesInfoQuery) + .then(result => { + const milestones = result && result.data && result.data.repository && result.data.repository.milestones && result.data.repository.milestones.nodes || []; + const matchingMilestone = _.find(milestones, m => m.title === selectedMilestone); + if (!matchingMilestone) { + throw new Error(`Unable to find milestone ${selectedMilestone} in the milestones. Current milestones info is: ${JSON.stringify(milestones, null, 2)}`); + } + return matchingMilestone.number; + }) + .then((milestone) => { + const getItemsForMilestoneQuery = { + query: `{ + repository(owner:"NativeScript", name:"nativescript-cli") { + milestone(number: ${milestone}) { + number + id + issuePrioritiesDebug + url + issues(first: 100) { + nodes { + title + url + number + labels(first:100) { + edges { + node { + name + } + } + } + projectCards(first: 100) { + nodes { + column { + name + } + project { + name + number + } + state + } + } + } + } + } + } +}` + }; + return sendRequest(getItemsForMilestoneQuery); + }) + .then((milestoneQueryResult) => { + const issues = (milestoneQueryResult && milestoneQueryResult.data && milestoneQueryResult.data.repository && + milestoneQueryResult.data.repository.milestone && milestoneQueryResult.data.repository.milestone.issues && + milestoneQueryResult.data.repository.milestone.issues.nodes) || []; + const finalIssuesForChangelog = []; + issues.forEach((issue) => { + const labels = ((issue.labels && issue.labels.edges) || []).map((lblObj) => lblObj && lblObj.node && lblObj.node.name); + const isFeature = labels.indexOf("feature") !== -1; + const isBug = labels.indexOf("bug") !== -1; + const shouldBeSkipped = labels.indexOf("no-changelog") !== -1; + if (isFeature && isBug) { + console.error(`The item '${issue.title}' has both bug and feature label. Clear one of them and try again.`.red); + process.exit(1); + } else if (shouldBeSkipped) { + console.log(`Item ${issue && issue.url}(${issue && issue.title}) will not be included in changelog as it has no-changelog label`.yellow); + } else { + // check if we have resolved it: + const columns = (issue && issue.projectCards && issue.projectCards.nodes || []).map(c => c && c.column && c.column.name); + // There shouldn't be more than one columns. + const column = _.first(columns); + if (columns && column === "Ready for Test" || column === "In Testing" || column === "Done") { + finalIssuesForChangelog.push({ + type: isFeature ? "feature" : "bug", + number: issue && issue.number, + title: issue && issue.title, + url: issue && issue.url + }); + } else { + console.log(`Item ${issue && issue.url}(${issue && issue.title}) will not be included in changelog as its status is ${columns}`.yellow); + } + } + }); + + return finalIssuesForChangelog; + }) + .then(data => { + const features = []; + const bugs = []; + + _.sortBy(data, (d) => d.number) + .forEach(d => { + if (d.type === "feature") { + features.push(`* [Implemented #${d.number}](${d.url}): ${d.title}`); + } else { + bugs.push(`* [Fixed #${d.number}](${d.url}): ${d.title}`); + } + }); + + const pathToChangelog = path.join(__dirname, "CHANGELOG.md"); + let changelogContent = fs.readFileSync(pathToChangelog).toString(); + + if (features.length === 0 && bugs.length === 0) { + console.error(`Unable to find anything ready for milestone ${selectedMilestone}`.red); + process.exit(2); + } + + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + const currentDate = new Date(); + + let newChangelogContent = `\n${selectedMilestone} (${currentDate.getFullYear()}, ${monthNames[currentDate.getMonth()]} ${currentDate.getDate()}) +=== +`; + if (features.length > 0) { + newChangelogContent += ` +### New + +${features.join("\n")} +`; + } + if (bugs.length) { + newChangelogContent += ` +### Fixed + +${bugs.join("\n")} +`; + } + + changelogContent = changelogContent.replace(/(NativeScript CLI Changelog\r?\n=+\r?\n)([\s\S]*)/m, `$1${newChangelogContent}\n$2`); + fs.writeFileSync(pathToChangelog, changelogContent); + console.log(`Successfully added Changelog for ${selectedMilestone}`.green); + console.log("Commit the local changes and send a PR.".magenta); + }) + .catch(error => console.error(error)); \ No newline at end of file diff --git a/lib/controllers/migrate-controller.ts b/lib/controllers/migrate-controller.ts index 0b7d041408..ec49989a85 100644 --- a/lib/controllers/migrate-controller.ts +++ b/lib/controllers/migrate-controller.ts @@ -163,11 +163,14 @@ Running this command will ${MigrateController.COMMON_MIGRATE_MESSAGE}`; const cachedResult = await this.getCachedShouldMigrate(projectDir, platform); if (cachedResult !== false) { remainingPlatforms.push(platform); + } else { + this.$logger.trace(`Got cached result for shouldMigrate for platform: ${platform}`); } } if (remainingPlatforms.length > 0) { shouldMigrate = await this._shouldMigrate({ projectDir, platforms: remainingPlatforms, allowInvalidVersions }); + this.$logger.trace(`Executed shouldMigrate for platforms: ${remainingPlatforms}. Result is: ${shouldMigrate}`); if (!shouldMigrate) { for (const remainingPlatform of remainingPlatforms) { diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index d8efcf13e5..e33ee726be 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -142,11 +142,23 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "*", "-R"); + // TODO: Check if we actually need this and if it should be targetSdk or compileSdk this.cleanResValues(targetSdkVersion, projectData); } + private getResDestinationDir(projectData: IProjectData): string { + const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated(projectData.getAppResourcesDirectoryPath()); + + if (appResourcesDirStructureHasMigrated) { + const appResourcesDestinationPath = this.getUpdatedAppResourcesDestinationDirPath(projectData); + return path.join(appResourcesDestinationPath, constants.MAIN_DIR, constants.RESOURCES_DIR); + } else { + return this.getLegacyAppResourcesDestinationDirPath(projectData); + } + } + private cleanResValues(targetSdkVersion: number, projectData: IProjectData): void { - const resDestinationDir = this.getAppResourcesDestinationDirectoryPath(projectData); + const resDestinationDir = this.getResDestinationDir(projectData); const directoriesInResFolder = this.$fs.readDirectory(resDestinationDir); const directoriesToClean = directoriesInResFolder .map(dir => { @@ -283,7 +295,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const projectAppResourcesPath = projectData.getAppResourcesDirectoryPath(projectData.projectDir); const platformsAppResourcesPath = this.getAppResourcesDestinationDirectoryPath(projectData); - this.cleanUpPreparedResources(projectAppResourcesPath, projectData); + this.cleanUpPreparedResources(projectData); this.$fs.ensureDirectoryExists(platformsAppResourcesPath); @@ -296,6 +308,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject // App_Resources/Android/libs is reserved to user's aars and jars, but they should not be copied as resources this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, "libs")); } + + const androidToolsInfo = this.$androidToolsInfo.getToolsInfo({ projectDir: projectData.projectDir }); + const compileSdkVersion = androidToolsInfo && androidToolsInfo.compileSdkVersion; + this.cleanResValues(compileSdkVersion, projectData); } public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { @@ -431,18 +447,31 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return path.join(this.getPlatformData(projectData).projectRoot, ...resourcePath); } - private cleanUpPreparedResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { - let resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); + /** + * The purpose of this method is to delete the previously prepared user resources. + * The content of the `/android/.../res` directory is based on user's resources and gradle project template from android-runtime. + * During preparation of the `/Android` we want to clean all the users files from previous preparation, + * but keep the ones that were introduced during `platform add` of the android-runtime. + * Currently the Gradle project template contains resources only in values and values-v21 directories. + * So the current logic of the method is cleaning al resources from `/android/.../res` that are not in `values.*` directories + * and that exist in the `/Android/.../res` directory + * This means that if user has a resource file in values-v29 for example, builds the project and then deletes this resource, + * it will be kept in platforms directory. Reference issue: `https://github.com/NativeScript/nativescript-cli/issues/5083` + * Same is valid for files in `drawable-` directories - in case in user's resources there's drawable-hdpi directory, + * which is deleted after the first build of app, it will remain in platforms directory. + */ + private cleanUpPreparedResources(projectData: IProjectData): void { + let resourcesDirPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); if (this.$androidResourcesMigrationService.hasMigrated(projectData.appResourcesDirectoryPath)) { - resourcesDirPath = path.join(resourcesDirPath, constants.MAIN_DIR, constants.RESOURCES_DIR); + resourcesDirPath = path.join(resourcesDirPath, constants.SRC_DIR, constants.MAIN_DIR, constants.RESOURCES_DIR); } const valuesDirRegExp = /^values/; if (this.$fs.exists(resourcesDirPath)) { const resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp)); - const appResourcesDestinationDirectoryPath = this.getAppResourcesDestinationDirectoryPath(projectData); - _.each(resourcesDirs, resourceDir => { - this.$fs.deleteDirectory(path.join(appResourcesDestinationDirectoryPath, resourceDir)); + const resDestinationDir = this.getResDestinationDir(projectData); + _.each(resourcesDirs, currentResource => { + this.$fs.deleteDirectory(path.join(resDestinationDir, currentResource)); }); } } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index db02f78245..30dc8b0cd3 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -282,8 +282,15 @@ export class ProjectDataService implements IProjectDataService { } }); - if (!foundMatchingDefinition && image.filename) { - this.$logger.warn(`Didn't find a matching image definition for file ${path.join(path.basename(dirPath), image.filename)}. This file will be skipped from reources generation.`); + if (!foundMatchingDefinition) { + if (image.height && image.width) { + this.$logger.trace("Missing data for image", image, " in CLI's resource file, but we will try to generate images based on the size from Contents.json"); + finalContent.images.push(image); + } else if (image.filename) { + this.$logger.warn(`Didn't find a matching image definition for file ${path.join(path.basename(dirPath), image.filename)}. This file will be skipped from resources generation.`); + } else { + this.$logger.trace(`Unable to detect data for image generation of image`, image); + } } }); diff --git a/lib/services/webpack/webpack-compiler-service.ts b/lib/services/webpack/webpack-compiler-service.ts index 4f0dd015da..e46474bdf8 100644 --- a/lib/services/webpack/webpack-compiler-service.ts +++ b/lib/services/webpack/webpack-compiler-service.ts @@ -42,7 +42,12 @@ export class WebpackCompilerService extends EventEmitter implements IWebpackComp if (message.emittedFiles) { if (isFirstWebpackWatchCompilation) { isFirstWebpackWatchCompilation = false; - this.expectedHashes[platformData.platformNameLowerCase] = message.hash; + this.expectedHashes[platformData.platformNameLowerCase] = prepareData.hmr ? message.hash : ""; + return; + } + + // the hash of the compilation is the same as the previous one + if (this.expectedHashes[platformData.platformNameLowerCase] === message.hash) { return; } diff --git a/resources/assets/image-definitions.json b/resources/assets/image-definitions.json index 7777b0ec04..78e1e7eb5b 100644 --- a/resources/assets/image-definitions.json +++ b/resources/assets/image-definitions.json @@ -1,6 +1,27 @@ { "ios": { "icons": [ + { + "width": 20, + "height": 20, + "directory": "Assets.xcassets/AppIcon.appiconset", + "filename": "icon-20@3x.png", + "scale": "3x" + }, + { + "width": 20, + "height": 20, + "directory": "Assets.xcassets/AppIcon.appiconset", + "filename": "icon-20@2x.png", + "scale": "2x" + }, + { + "width": 20, + "height": 20, + "directory": "Assets.xcassets/AppIcon.appiconset", + "filename": "icon-20.png", + "scale": "1x" + }, { "width": 60, "height": 60, diff --git a/test/services/android-project-service.ts b/test/services/android-project-service.ts index d25b51982f..b541c182a4 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -3,6 +3,7 @@ import { Yok } from "../../lib/common/yok"; import * as stubs from "../stubs"; import { assert } from "chai"; import * as sinon from "sinon"; +import * as path from "path"; import { GradleCommandService } from "../../lib/services/android/gradle-command-service"; import { GradleBuildService } from "../../lib/services/android/gradle-build-service"; import { GradleBuildArgsService } from "../../lib/services/android/gradle-build-args-service"; @@ -42,7 +43,7 @@ const createTestInjector = (): IInjector => { testInjector.register("gradleBuildService", GradleBuildService); testInjector.register("gradleBuildArgsService", GradleBuildArgsService); testInjector.register("analyticsService", stubs.AnalyticsService); - testInjector.register("staticConfig", {TRACK_FEATURE_USAGE_SETTING_NAME: "TrackFeatureUsage"}); + testInjector.register("staticConfig", { TRACK_FEATURE_USAGE_SETTING_NAME: "TrackFeatureUsage" }); return testInjector; }; @@ -59,7 +60,7 @@ const getDefautlBuildConfig = (): IBuildConfig => { }; }; -describe("androidDeviceDebugService", () => { +describe("androidProjectService", () => { let injector: IInjector; let androidProjectService: IPlatformProjectService; let sandbox: sinon.SinonSandbox = null; @@ -74,7 +75,7 @@ describe("androidDeviceDebugService", () => { sandbox.restore(); }); - describe("buildPlatform", () => { + describe("buildProject", () => { let projectData: IProjectData; let childProcess: stubs.ChildProcessStub; @@ -138,4 +139,182 @@ describe("androidDeviceDebugService", () => { assert.include(childProcess.lastCommandArgs, "bundleDebug"); }); }); + + describe("prepareAppResources", () => { + const projectDir = "testDir"; + const pathToAppResourcesDir = path.join(projectDir, "app", "App_Resources"); + const pathToAppResourcesAndroid = path.join(pathToAppResourcesDir, "Android"); + const pathToPlatformsAndroid = path.join(projectDir, "platforms", "android"); + const pathToResDirInPlatforms = path.join(pathToPlatformsAndroid, "app", "src", "main", "res"); + const valuesV27Path = path.join(pathToResDirInPlatforms, "values-v27"); + const valuesV28Path = path.join(pathToResDirInPlatforms, "values-v28"); + const libsPath = path.join(pathToResDirInPlatforms, "libs"); + const drawableHdpiPath = path.join(pathToResDirInPlatforms, "drawable-hdpi"); + const drawableLdpiPath = path.join(pathToResDirInPlatforms, "drawable-ldpi"); + let deletedDirs: string[] = []; + let copiedFiles: { sourceFileName: string, destinationFileName: string }[] = []; + let readDirectoryResults: IDictionary = {}; + let fs: IFileSystem = null; + let projectData: IProjectData = null; + let compileSdkVersion = 29; + + beforeEach(() => { + projectData = injector.resolve("projectData"); + projectData.projectDir = projectDir; + projectData.appResourcesDirectoryPath = pathToAppResourcesDir; + + deletedDirs = []; + copiedFiles = []; + readDirectoryResults = {}; + + fs = injector.resolve("fs"); + fs.deleteDirectory = (directory: string): void => { + deletedDirs.push(directory); + }; + fs.copyFile = (sourceFileName: string, destinationFileName: string): void => { + copiedFiles.push({ sourceFileName, destinationFileName }); + }; + fs.readDirectory = (dir: string): string[] => { + return readDirectoryResults[dir] || []; + }; + + compileSdkVersion = 29; + + const androidToolsInfo = injector.resolve("androidToolsInfo"); + androidToolsInfo.getToolsInfo = (config?: IProjectDir): IAndroidToolsInfoData => { + return { + compileSdkVersion + }; + }; + + }); + + describe("when new Android App_Resources structure is detected (post {N} 4.0 structure)", () => { + const pathToSrcDirInAppResources = path.join(pathToAppResourcesAndroid, "src"); + beforeEach(() => { + const androidResourcesMigrationService = injector.resolve("androidResourcesMigrationService"); + androidResourcesMigrationService.hasMigrated = () => true; + }); + + it("copies everything from App_Resources/Android/src to correct location in platforms", async () => { + await androidProjectService.prepareAppResources(projectData); + + assert.deepEqual(copiedFiles, [{ sourceFileName: path.join(pathToSrcDirInAppResources, "*"), destinationFileName: path.join(projectData.projectDir, "platforms", "android", "app", "src") }]); + }); + + it("deletes correct values- directories based on the compileSdk", async () => { + readDirectoryResults = { + [`${pathToResDirInPlatforms}`]: [ + "values", + "values-v21", + "values-v26", + "values-v27", + "values-v28" + ] + }; + + compileSdkVersion = 26; + await androidProjectService.prepareAppResources(projectData); + assert.deepEqual(deletedDirs, [ + valuesV27Path, + valuesV28Path + ]); + }); + + it("deletes drawable directories when they've been previously prepared", async () => { + readDirectoryResults = { + [path.join(pathToSrcDirInAppResources, "main", "res")]: [ + "drawable-hdpi", + "drawable-ldpi", + "values", + "values-v21", + "values-v29" + ], + [`${pathToResDirInPlatforms}`]: [ + "drawable-hdpi", + "drawable-ldpi", + "drawable-mdpi", + "values", + "values-v21", + "values-v29" + ] + }; + + await androidProjectService.prepareAppResources(projectData); + + // NOTE: Currently the drawable-mdpi directory is not deleted from prepared App_Resources as it does not exist in the currently prepared App_Resources + // This is not correct behavior and it should be fixed in a later point. + assert.deepEqual(deletedDirs, [ + drawableHdpiPath, + drawableLdpiPath + ]); + }); + }); + + describe("when old Android App_Resources structure is detected (post {N} 4.0 structure)", () => { + beforeEach(() => { + const androidResourcesMigrationService = injector.resolve("androidResourcesMigrationService"); + androidResourcesMigrationService.hasMigrated = () => false; + }); + + it("copies everything from App_Resources/Android to correct location in platforms", async () => { + await androidProjectService.prepareAppResources(projectData); + + assert.deepEqual(copiedFiles, [{ sourceFileName: path.join(pathToAppResourcesAndroid, "*"), destinationFileName: pathToResDirInPlatforms }]); + }); + + it("deletes correct values- directories based on the compileSdk", async () => { + readDirectoryResults = { + [`${pathToResDirInPlatforms}`]: [ + "values", + "values-v21", + "values-v26", + "values-v27", + "values-v28" + ] + }; + + compileSdkVersion = 26; + + await androidProjectService.prepareAppResources(projectData); + + // During preparation of old App_Resources, CLI copies all of them in platforms and after that deletes the libs directory. + assert.deepEqual(deletedDirs, [ + libsPath, + valuesV27Path, + valuesV28Path + ]); + }); + + it("deletes drawable directories when they've been previously prepared", async () => { + readDirectoryResults = { + [`${pathToAppResourcesAndroid}`]: [ + "drawable-hdpi", + "drawable-ldpi", + "values", + "values-v21", + "values-v29" + ], + [`${pathToResDirInPlatforms}`]: [ + "drawable-hdpi", + "drawable-ldpi", + "drawable-mdpi", + "values", + "values-v21", + "values-v29" + ] + }; + + await androidProjectService.prepareAppResources(projectData); + // NOTE: Currently the drawable-mdpi directory is not deleted from prepared App_Resources as it does not exist in the currently prepared App_Resources + // This is not correct behavior and it should be fixed in a later point. + // During preparation of old App_Resources, CLI copies all of them in platforms and after that deletes the libs directory. + assert.deepEqual(deletedDirs, [ + drawableHdpiPath, + drawableLdpiPath, + libsPath + ]); + }); + }); + }); }); diff --git a/test/services/project-data-service.ts b/test/services/project-data-service.ts index 35c4c919c2..9b4da40ea7 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -366,6 +366,103 @@ describe("projectDataService", () => { assert.deepEqual(assetStructure, { ios: emptyAssetStructure, android: _.merge(_.cloneDeep(emptyAssetStructure), { splashImages: null }) }); }); + + it("generates iOS resources for files, which are not declared in image-definitions.json, but we have their size from resource's Contents.json", async () => { + const defaultEmptyData: any = {}; + defaultEmptyData[CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}; + const testInjector = createTestInjector(JSON.stringify(defaultEmptyData)); + const fs = testInjector.resolve("fs"); + fs.exists = () => true; + fs.readJson = (filePath: string): any => { + if (basename(filePath) === AssetConstants.imageDefinitionsFileName) { + return { + android: {}, + ios: { + "icons": [ + { + "width": 20, + "height": 20, + "directory": "Assets.xcassets/AppIcon.appiconset", + "filename": "icon-20@3x.png", + "scale": "3x" + }] + } + }; + } + + if (basename(filePath) === AssetConstants.iOSResourcesFileName && filePath.indexOf(AssetConstants.iOSIconsDirName) !== -1) { + return { + "images": [ + { + "size": "20x20", + "idiom": "iphone", + "filename": "icon-20@2x.png", + "scale": "2x" + }, + { + "size": "20x20", + "idiom": "iphone", + "filename": "icon-20@3x.png", + "scale": "3x" + } + ] + }; + } + }; + + const projectDataService = testInjector.resolve("projectDataService"); + const assetStructure = await projectDataService.getAssetsStructure({ projectDir: "." }); + const emptyAssetStructure: IAssetGroup = { + icons: { + images: [] + }, + splashBackgrounds: { + images: [] + }, + splashCenterImages: { + images: [] + }, + splashImages: { + images: [] + } + }; + + const expectedIOSStructure = _.merge({}, emptyAssetStructure, { + icons: { + "images": [ + { + "filename": "icon-20@2x.png", + "height": 20, + "idiom": "iphone", + "scale": "2x", + "size": "20x20", + "width": 20 + }, + { + "filename": "icon-20@3x.png", + "height": 20, + "idiom": "iphone", + "overlayImageScale": undefined, + "resizeOperation": undefined, + "rgba": undefined, + "scale": "3x", + "size": "20x20", + "width": 20 + } + ] + } + }); + + _.each(assetStructure.ios.icons.images, icon => { + // as path is generated from the current directory, skip it from the validation + delete icon.path; + }); + + assert.deepEqual(assetStructure, { + ios: expectedIOSStructure, + android: _.merge(_.cloneDeep(emptyAssetStructure), { splashImages: null }) + }); + }); }); describe("getAppExecutableFiles", () => {