From 384b1fd654637d92827d40a7755ad52b46b1068d Mon Sep 17 00:00:00 2001 From: Murat Mehmet Date: Tue, 16 Jul 2024 20:58:57 +0300 Subject: [PATCH] feat: added gradle properties task support --- .../unit/tasks/gradlePropertiesTask.spec.ts | 342 ++++++++++++++++++ src/__tests__/unit/utils/runTask.spec.ts | 3 + src/constants.ts | 1 + src/schema/integrate.schema.json | 38 ++ src/schema/upgrade.schema.json | 38 ++ src/tasks/gradlePropertiesTask.ts | 109 ++++++ src/types/mod.types.ts | 8 + src/utils/taskManager.ts | 2 + .../android-tasks/gradle-properties.md | 67 ++++ 9 files changed, 608 insertions(+) create mode 100644 src/__tests__/unit/tasks/gradlePropertiesTask.spec.ts create mode 100644 src/tasks/gradlePropertiesTask.ts create mode 100644 website/docs/for-developers/guides/task-types/android-tasks/gradle-properties.md diff --git a/src/__tests__/unit/tasks/gradlePropertiesTask.spec.ts b/src/__tests__/unit/tasks/gradlePropertiesTask.spec.ts new file mode 100644 index 0000000..65b61ab --- /dev/null +++ b/src/__tests__/unit/tasks/gradlePropertiesTask.spec.ts @@ -0,0 +1,342 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +const { mockFs, mockPrompter } = require('../../mocks/mockAll'); + +import path from 'path'; +import { Constants } from '../../../constants'; +import { + gradlePropertiesTask, + runTask, +} from '../../../tasks/gradlePropertiesTask'; +import { GradlePropertiesTaskType } from '../../../types/mod.types'; + +describe('gradlePropertiesTask', () => { + it('should prepend text into empty body ', async () => { + let content = ''; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + append: 'ignoredfile', + prepend: 'ignoredfile', + }, + ], + }; + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(content).toEqual(` +ignoredfile +`); + }); + it('should prepend text into empty body without block', async () => { + let content = ''; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + append: 'ignoredfile', + prepend: 'ignoredfile', + }, + ], + }; + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(content).toEqual(` +ignoredfile +`); + }); + it('should skip insert when ifNotPresent exists', async () => { + const content = ` +someignored +`; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + ifNotPresent: 'someignored', + prepend: 'ignored', + }, + { + ifNotPresent: 'someignored', + append: 'ignored', + }, + ], + }; + + await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(mockPrompter.log.message).toHaveBeenCalledWith( + expect.stringContaining('found existing ') + ); + }); + it('should prepend text into partial body ', async () => { + let content = ` +ignoredfile +`; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + prepend: 'ignoredfile', + }, + ], + }; + + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(content).toEqual(` +ignoredfile +`); + }); + it('should append text into existing body ', async () => { + let content = ` +ignoredfile +`; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + append: 'config2 = use_native_modules!', + }, + ], + }; + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(content).toEqual(` +ignoredfile +config2 = use_native_modules! +`); + }); + it('should insert text after point with comment', async () => { + let content = ` +ignoredfile +`; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + after: 'config', + prepend: 'config2 = use_native_modules!', + comment: 'test comment', + }, + ], + }; + + content = await gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }); + expect(content).toContain(` +ignoredfile +`); + }); + + it('should skip if condition not met', async () => { + const content = ''; + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + when: { test: 'random' }, + prepend: 'random;', + }, + ], + }; + + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: task, + content, + packageName: 'test-package', + }) + ).resolves.not.toThrowError('target not found'); + }); + + describe('runTask', () => { + it('should read and write gradleProperties file', async () => { + let content = ` +test1; +test3; +`; + const gradlePropertiesPath = path.resolve( + __dirname, + `../../mock-project/android/${Constants.GRADLE_PROPERTIES_FILE_NAME}` + ); + mockFs.writeFileSync(gradlePropertiesPath, content); + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + prepend: 'test2;', + }, + ], + }; + + await runTask({ + configPath: 'path/to/config', + task: task, + packageName: 'test-package', + }); + content = mockFs.readFileSync(gradlePropertiesPath); + // @ts-ignore + expect(content).toContain(task.actions[0].prepend); + }); + it('should throw when insertion point not found with strict', async () => { + const content = ` +test1; +test3; +`; + const taskInsertBefore: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + before: 'random', + append: 'test2;', + strict: true, + }, + ], + }; + const taskInsertBeforeNonStrict: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + before: 'random', + append: 'test2;', + }, + ], + }; + + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: taskInsertBefore, + content, + packageName: 'test-package', + }) + ).rejects.toThrowError('insertion point'); + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: taskInsertBeforeNonStrict, + content, + packageName: 'test-package', + }) + ).resolves.not.toThrowError('insertion point'); + const taskInsertAfter: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + after: 'random', + prepend: 'test2;', + strict: true, + }, + ], + }; + + const taskInsertAfterNonStrict: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + after: 'random', + prepend: 'test2;', + }, + ], + }; + + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: taskInsertAfter, + content, + packageName: 'test-package', + }) + ).rejects.toThrowError('insertion point'); + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: taskInsertAfterNonStrict, + content, + packageName: 'test-package', + }) + ).resolves.not.toThrowError('insertion point'); + }); + it('should throw when block is used', async () => { + const content = ` +test1; +test3; +`; + const taskInsertBefore: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + block: 'test', + before: 'random', + append: 'test2;', + strict: true, + }, + ], + }; + await expect( + gradlePropertiesTask({ + configPath: 'path/to/config', + task: taskInsertBefore, + content, + packageName: 'test-package', + }) + ).rejects.toThrowError('block is not supported'); + }); + it('should work when gradleProperties does not exist', async () => { + const task: GradlePropertiesTaskType = { + task: 'gradle_properties', + actions: [ + { + prepend: 'test2;', + }, + ], + }; + + await expect( + runTask({ + configPath: 'path/to/config', + task: task, + packageName: 'test-package', + }) + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/unit/utils/runTask.spec.ts b/src/__tests__/unit/utils/runTask.spec.ts index e5c2bd8..0ed8503 100644 --- a/src/__tests__/unit/utils/runTask.spec.ts +++ b/src/__tests__/unit/utils/runTask.spec.ts @@ -26,6 +26,9 @@ const mocks = { android_manifest: { runTask: jest.fn(), }, + gradle_properties: { + runTask: jest.fn(), + }, podfile: { runTask: jest.fn(), }, diff --git a/src/constants.ts b/src/constants.ts index 0150f46..5c08ac4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,6 +21,7 @@ export const Constants = { MAIN_ACTIVITY_KT_FILE_NAME: 'MainActivity.kt', POD_FILE_NAME: 'Podfile', GITIGNORE_FILE_NAME: '.gitignore', + GRADLE_PROPERTIES_FILE_NAME: 'gradle.properties', XCODEPROJ_EXT: '.xcodeproj', CONFIG_FILE_NAME: 'integrate.yml', XCODE_APPLICATION_TYPE: 'com.apple.product-type.application', diff --git a/src/schema/integrate.schema.json b/src/schema/integrate.schema.json index 0083f88..bc99186 100644 --- a/src/schema/integrate.schema.json +++ b/src/schema/integrate.schema.json @@ -530,6 +530,41 @@ ], "type": "object" }, + "GradlePropertiesTaskType": { + "additionalProperties": false, + "properties": { + "actions": { + "items": { + "$ref": "#/definitions/ContentModifierType" + }, + "type": "array" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "postInfo": { + "$ref": "#/definitions/TextOrTitleMessage" + }, + "preInfo": { + "$ref": "#/definitions/TextOrTitleMessage" + }, + "task": { + "const": "gradle_properties", + "type": "string" + }, + "when": { + "$ref": "#/definitions/AnyObject" + } + }, + "required": [ + "actions", + "task" + ], + "type": "object" + }, "JsonTaskType": { "additionalProperties": false, "properties": { @@ -689,6 +724,9 @@ { "$ref": "#/definitions/GitignoreTaskType" }, + { + "$ref": "#/definitions/GradlePropertiesTaskType" + }, { "$ref": "#/definitions/FsTaskType" }, diff --git a/src/schema/upgrade.schema.json b/src/schema/upgrade.schema.json index 8e8034c..3d9e95b 100644 --- a/src/schema/upgrade.schema.json +++ b/src/schema/upgrade.schema.json @@ -530,6 +530,41 @@ ], "type": "object" }, + "GradlePropertiesTaskType": { + "additionalProperties": false, + "properties": { + "actions": { + "items": { + "$ref": "#/definitions/ContentModifierType" + }, + "type": "array" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "postInfo": { + "$ref": "#/definitions/TextOrTitleMessage" + }, + "preInfo": { + "$ref": "#/definitions/TextOrTitleMessage" + }, + "task": { + "const": "gradle_properties", + "type": "string" + }, + "when": { + "$ref": "#/definitions/AnyObject" + } + }, + "required": [ + "actions", + "task" + ], + "type": "object" + }, "JsonTaskType": { "additionalProperties": false, "properties": { @@ -689,6 +724,9 @@ { "$ref": "#/definitions/GitignoreTaskType" }, + { + "$ref": "#/definitions/GradlePropertiesTaskType" + }, { "$ref": "#/definitions/FsTaskType" }, diff --git a/src/tasks/gradlePropertiesTask.ts b/src/tasks/gradlePropertiesTask.ts new file mode 100644 index 0000000..eae2793 --- /dev/null +++ b/src/tasks/gradlePropertiesTask.ts @@ -0,0 +1,109 @@ +import fs from 'fs'; +import path from 'path'; +import { Constants } from '../constants'; +import { BlockContentType, GradlePropertiesTaskType } from '../types/mod.types'; +import { applyContentModification } from '../utils/applyContentModification'; +import { getErrMessage } from '../utils/getErrMessage'; +import { getProjectPath } from '../utils/getProjectPath'; +import { satisfies } from '../utils/satisfies'; +import { setState } from '../utils/setState'; +import { variables } from '../variables'; + +export async function gradlePropertiesTask(args: { + configPath: string; + packageName: string; + content: string; + task: GradlePropertiesTaskType; +}): Promise { + let { content } = args; + const { task, configPath, packageName } = args; + + for (const action of task.actions) { + variables.set('CONTENT', content); + if (action.when && !satisfies(variables.getStore(), action.when)) { + setState(action.name, { + state: 'skipped', + reason: 'when', + error: false, + }); + continue; + } + + setState(action.name, { + state: 'progress', + error: false, + }); + try { + content = await applyContentModification({ + action, + findOrCreateBlock, + configPath, + packageName, + content, + indentation: 0, + buildComment: buildGradlePropertiesComment, + }); + setState(action.name, { + state: 'done', + error: false, + }); + } catch (e) { + setState(action.name, { + state: 'error', + reason: getErrMessage(e), + error: true, + }); + throw e; + } + } + return content; +} + +function findOrCreateBlock(): { + blockContent: BlockContentType; + content: string; +} { + throw new Error('block is not supported in gradleProperties'); +} + +function buildGradlePropertiesComment(comment: string): string[] { + return comment.split('\n').map(x => `# ${x}`); +} + +function getGradlePropertiesPath() { + const projectPath = getProjectPath(); + + return path.join( + projectPath, + 'android', + Constants.GRADLE_PROPERTIES_FILE_NAME + ); +} + +function readGradlePropertiesContent() { + const gradlePropertiesPath = getGradlePropertiesPath(); + if (!fs.existsSync(gradlePropertiesPath)) return ''; + return fs.readFileSync(gradlePropertiesPath, 'utf-8'); +} + +function writeGradlePropertiesContent(content: string): void { + const gradlePropertiesPath = getGradlePropertiesPath(); + return fs.writeFileSync(gradlePropertiesPath, content, 'utf-8'); +} + +export async function runTask(args: { + configPath: string; + packageName: string; + task: GradlePropertiesTaskType; +}): Promise { + let content = readGradlePropertiesContent(); + + content = await gradlePropertiesTask({ + ...args, + content, + }); + + writeGradlePropertiesContent(content); +} + +export const summary = 'gradle.properties modification'; diff --git a/src/types/mod.types.ts b/src/types/mod.types.ts index 241177c..24dd7fe 100644 --- a/src/types/mod.types.ts +++ b/src/types/mod.types.ts @@ -357,6 +357,13 @@ export type GitignoreTaskType = ModTaskBase & task: 'gitignore'; }; +// gradle properties task + +export type GradlePropertiesTaskType = ModTaskBase & + ActionsType & { + task: 'gradle_properties'; + }; + // fs task export type FsTaskType = ModTaskBase & @@ -423,6 +430,7 @@ export type ModStep = | XcodeTaskType | PodFileTaskType | GitignoreTaskType + | GradlePropertiesTaskType | FsTaskType | JsonTaskType | PromptTaskType diff --git a/src/utils/taskManager.ts b/src/utils/taskManager.ts index 8ea1486..cdce398 100644 --- a/src/utils/taskManager.ts +++ b/src/utils/taskManager.ts @@ -6,6 +6,7 @@ import * as android_manifest from '../tasks/androidManifestTask'; import * as strings_xml from '../tasks/stringsXmlTask'; import * as podfile from '../tasks/podFileTask'; import * as gitignore from '../tasks/gitignoreTask'; +import * as gradleProperties from '../tasks/gradlePropertiesTask'; import * as fs from '../tasks/fsTask'; import * as json from '../tasks/jsonTask'; import * as prompt from '../tasks/promptTask'; @@ -26,6 +27,7 @@ const task: Record = { android_manifest, podfile, gitignore, + gradleProperties, fs, json, prompt, diff --git a/website/docs/for-developers/guides/task-types/android-tasks/gradle-properties.md b/website/docs/for-developers/guides/task-types/android-tasks/gradle-properties.md new file mode 100644 index 0000000..a077ba7 --- /dev/null +++ b/website/docs/for-developers/guides/task-types/android-tasks/gradle-properties.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 7 +title: gradle.properties +--- + +# Gradle Properties Task Configuration (`gradle_properties`) + +_Modify gradle.properties file_ + +The `gradle_properties` task allows you to customize gradle.properties file, which is used to ignore some files from getting added to version control. + +## Task Properties + +| Property | Type | Description | +|:---------|:------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| task | "gradle_properties", required | Specifies the task type, which should be set to "gradle_properties" for this task. | +| name | string | An optional name for the task. If provided, the task state will be saved as a variable. Visit [Task and Action States](../../states) page to learn more. | +| label | string | An optional label or description for the task. | +| when | object | Visit [Conditional Tasks and Actions](../../when) page to learn how to execute task conditionally. | +| actions | Array\<[Action](#action-properties)\>, required | An array of action items that define the modifications to be made in the file. | + +## Action Properties + +### Common properties + +| Property | Type | Description | +|:---------|:-------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | An optional name for the task. If provided, the task state will be saved as a variable. Visit [Task and Action States](../../states) page to learn more. | +| when | object | Visit [Conditional Tasks and Actions](../../when) page to learn how to execute action conditionally. | + +### Context reduction properties + +| Property | Type | Description | +|:---------|:-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| before | string or `{regex: string, flags: string}` | Text or code that is used to specify a point within the context where text should be inserted before. It can be a string or an object with a `regex` and `flags` field to perform a regex-based search. | +| after | string or `{regex: string, flags: string}` | Text or code that is used to specify a point within the context where text should be inserted after. It can be a string or an object with a `regex` and `flags` field to perform a regex-based search. | +| search | string or `{regex: string, flags: string}` | A string or object (with regex and flags) that narrows the context to a specific text within the method or file. | + +### Context modification properties + +| Property | Type | Description | +|:---------|:---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| prepend | string or `{file: string}` | Text or code to prepend at the beginning of the specified context. It can be a string or an object with a `file` field that points to a file containing the code to prepend. | +| append | string or `{file: string}` | Text or code to append at the end of the specified context. It can be a string or an object with a `file` field that points to a file containing the code to append. | +| replace | string or `{file: string}` | Text or code to replace the entire specified context. It can be a string or an object with a `file` field that points to a file containing the code to replace. | + +### Other properties + +| Property | Type | Description | +|:-------------|:--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| exact | boolean | A boolean flag that modifies the whitespace and new line management. | +| strict | boolean | Specifies the behavior of the `before` and `after` fields. If set to `true`, the task will throw an error if the text in the `before` or `after` field is not found in the context, otherwise, it will ignore the field. | +| ifNotPresent | string | Indicates that the task should only be executed if the specified text or code is not present within the specified context. | +| comment | string | An optional comment to add before the inserted code or text. The comment is purely informational and does not affect the code's functionality. | + +## Example + +Here's an example of a configuration file (`integrate.yml`) that utilizes the `gradle_properties` task to modify the gradle.properties file: + +```yaml +task: gradle_properties +label: "Modify gradle.properties" +actions: + - append: MAPBOX_DOWNLOADS_TOKEN=some_token +``` + +In this example, we append a token declaration within gradle.properties.