diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index b21bed27e7e3b..7e82957e69b90 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -15545,8 +15545,8 @@ exports["default"] = newComponentCategory; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -16045,24 +16045,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index cced1428f8159..b83484c8f1193 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12319,8 +12319,8 @@ exports.getStringInput = getStringInput; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12819,24 +12819,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index 8441be818a793..143802767ebdc 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -737038,8 +737038,8 @@ checkAndroidStatus() Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -737538,24 +737538,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 3c066552b7f90..9414a60a17251 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11602,8 +11602,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12102,24 +12102,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 4a91f265e5ec3..35e11a6f971ca 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -11668,8 +11668,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12406,24 +12406,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 7b060965099bd..5ac1f43f8795c 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11563,8 +11563,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12063,24 +12063,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 606c299062579..0de8f6c205dd9 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11705,8 +11705,8 @@ exports.getStringInput = getStringInput; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12443,24 +12443,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index fd865d2e5cfe7..156e01bfe1bdb 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -11560,8 +11560,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12298,24 +12298,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 8a3040210cd22..073e21e15a298 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11666,8 +11666,8 @@ exports.getStringInput = getStringInput; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12166,24 +12166,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index b78a120495c55..2f37338ceabb8 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11563,8 +11563,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12063,24 +12063,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index bde2438bbb804..e69b41fcff40e 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -12960,8 +12960,8 @@ exports.getStringInput = getStringInput; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -13460,24 +13460,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 30b0a8f1521b0..f93deb9eb5315 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11693,8 +11693,8 @@ exports["default"] = run; Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12193,24 +12193,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 3ed98f90c97e8..3f81ec10d4183 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -11650,8 +11650,8 @@ run().catch((error) => { Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12150,24 +12150,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 63a8f457c2591..06519c9102474 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11573,8 +11573,8 @@ reopenIssueWithComment() Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12073,24 +12073,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 66cf4eccfd8b2..e838f256d2394 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11665,8 +11665,8 @@ getNumberOfItemsFromReviewerChecklist() Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12165,24 +12165,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 0e90a96fc5fe8..878bdc738d424 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11605,8 +11605,8 @@ GithubUtils_1.default.octokit.pulls Object.defineProperty(exports, "__esModule", ({ value: true })); const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', }; const CONST = { @@ -12105,24 +12105,6 @@ class GithubUtils { }) .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path, ref = 'main') { - const { data } = await this.octokit.repos.getContent({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } /** * Get commits between two tags via the GitHub API */ diff --git a/.github/libs/CONST.ts b/.github/libs/CONST.ts index 441fc47ee943f..d85642c323594 100644 --- a/.github/libs/CONST.ts +++ b/.github/libs/CONST.ts @@ -1,8 +1,8 @@ const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); const GIT_CONST = { - GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER ?? 'Expensify', - APP_REPO: (process.env.GITHUB_REPOSITORY ?? 'Expensify/App').split('/').at(1) ?? '', + GITHUB_OWNER: process.env.GITHUB_REPOSITORY_OWNER, + APP_REPO: process.env.GITHUB_REPOSITORY.split('/').at(1) ?? '', MOBILE_EXPENSIFY_REPO: 'Mobile-Expensify', } as const; diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index 11bbacecca4a3..eddd2d9596bf0 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -570,25 +570,6 @@ class GithubUtils { .then((response) => response.url); } - /** - * Get the contents of a file from the API at a given ref as a string. - */ - static async getFileContents(path: string, ref = 'main'): Promise { - const {data} = await this.octokit.repos.getContent({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - path, - ref, - }); - if (Array.isArray(data)) { - throw new Error(`Provided path ${path} refers to a directory, not a file`); - } - if (!('content' in data)) { - throw new Error(`Provided path ${path} is invalid`); - } - return Buffer.from(data.content, 'base64').toString('utf8'); - } - /** * Get commits between two tags via the GitHub API */ diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index fc24860f8634a..a82c874208869 100755 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -8,12 +8,11 @@ import fs from 'fs'; import path from 'path'; import type {TemplateExpression} from 'typescript'; import ts from 'typescript'; -import GitHubUtils from '@github/libs/GithubUtils'; import decodeUnicode from '@libs/StringUtils/decodeUnicode'; import dedent from '@libs/StringUtils/dedent'; import hashStr from '@libs/StringUtils/hash'; import {isTranslationTargetLocale, LOCALES, TRANSLATION_TARGET_LOCALES} from '@src/CONST/LOCALES'; -import type {Locale, TranslationTargetLocale} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; import CLI from './utils/CLI'; import Prettier from './utils/Prettier'; import PromisePool from './utils/PromisePool'; @@ -21,15 +20,6 @@ import ChatGPTTranslator from './utils/Translator/ChatGPTTranslator'; import DummyTranslator from './utils/Translator/DummyTranslator'; import type Translator from './utils/Translator/Translator'; import TSCompilerUtils from './utils/TSCompilerUtils'; -import type {LabeledNode} from './utils/TSCompilerUtils'; - -/** - * This represents a string to translate. In the context of translation, two strings are considered equal only if their contexts are also equal. - */ -type StringWithContext = { - text: string; - context?: string; -}; const GENERATED_FILE_PREFIX = dedent(` /** @@ -45,6 +35,14 @@ const GENERATED_FILE_PREFIX = dedent(` */ `); +/** + * This represents a string to translate. In the context of translation, two strings are considered equal only if their contexts are also equal. + */ +type StringWithContext = { + text: string; + context?: string; +}; + /** * This class encapsulates most of the non-CLI logic to generate translations. * The primary reason it exists as a class is so we can import this file with no side effects at the top level of the script. @@ -57,9 +55,6 @@ const GENERATED_FILE_PREFIX = dedent(` * - It also formats the files using prettier. */ class TranslationGenerator { - /** - * Regex to match context annotations. - */ private static readonly CONTEXT_REGEX = /^\s*(?:\/{2}|\*|\/\*)?\s*@context\s+([^\n*/]+)/; /** @@ -82,133 +77,33 @@ class TranslationGenerator { */ private readonly translator: Translator; - /** - * Ref to use for existing translations. - */ - private readonly compareRef: string; - - /** - * Should we print verbose logs? - */ - private readonly verbose: boolean; - - /** - * If a complex template expression comes from an existing translation file rather than ChatGPT, then the hashes of its spans will be serialized from the translated version of those spans. - * This map provides us a way to look up the English hash for each translated span hash, so that when we're transforming the English file and we encounter a translated expression hash, - * we can look up English hash and use it to look up the translation for that hash (since the translation map is keyed by English string hashes). - */ - private readonly translatedSpanHashToEnglishSpanHash = new Map(); - - constructor(config: {targetLanguages: TranslationTargetLocale[]; languagesDir: string; sourceFile: string; translator: Translator; compareRef: string; verbose: boolean}) { + constructor(config: {targetLanguages: TranslationTargetLocale[]; languagesDir: string; sourceFile: string; translator: Translator}) { this.targetLanguages = config.targetLanguages; this.languagesDir = config.languagesDir; const sourceCode = fs.readFileSync(config.sourceFile, 'utf8'); this.sourceFile = ts.createSourceFile(config.sourceFile, sourceCode, ts.ScriptTarget.Latest, true); this.translator = config.translator; - this.compareRef = config.compareRef; - this.verbose = config.verbose; } public async generateTranslations(): Promise { const promisePool = new PromisePool(); - // map of translations for each locale - const translations = new Map>(); - - // If a compareRef is provided, fetch the old version of the files, and traverse the ASTs in parallel to extract existing translations - if (this.compareRef) { - const allLocales: Locale[] = [LOCALES.EN, ...this.targetLanguages]; - - // An array of labeled "translation nodes", where "translations node" refers to the main object in en.ts and - // other locale files that contains all the translations. - const oldTranslationNodes: Array> = []; - const downloadPromises = []; - for (const targetLanguage of allLocales) { - const targetPath = `src/languages/${targetLanguage}.ts`; - downloadPromises.push( - promisePool.add(() => - // Download the file from GitHub - GitHubUtils.getFileContents(targetPath, this.compareRef).then((content) => { - // Parse the file contents and find the translations node, save it in the oldTranslationsNodes map - const parsed = ts.createSourceFile(targetPath, content, ts.ScriptTarget.Latest, true); - const oldTranslationNode = this.findTranslationsNode(parsed); - if (!oldTranslationNode) { - throw new Error(`Could not find translation node in ${targetPath}`); - } - oldTranslationNodes.push({label: targetLanguage, node: oldTranslationNode}); - }), - ), - ); - } - await Promise.all(downloadPromises); - - // Traverse ASTs of all downloaded files in parallel, building a map of {locale => {translationKey => translation}} - // Note: traversing in parallel is not just a performance optimization. We need the translation key - // from en.ts to map to translations in other files, but we can't rely on dot-notation style paths alone - // because sometimes there are strings defined elsewhere, such as in functions or nested templates. - // So instead, we rely on the fact that the AST structure of en.ts will very nearly match the AST structure of other locales. - // We walk through the AST of en.ts in parallel with all the other ASTs, and take the translation key from - // en.ts and the translated value from the target locale. - TSCompilerUtils.traverseASTsInParallel(oldTranslationNodes, (nodes: Record) => { - const enNode = nodes[LOCALES.EN]; - if (!this.shouldNodeBeTranslated(enNode)) { - return; - } - - // Use English for the translation key - const translationKey = this.getTranslationKey(enNode); - - for (const targetLanguage of this.targetLanguages) { - const translatedNode = nodes[targetLanguage]; - if (!this.shouldNodeBeTranslated(translatedNode)) { - if (this.verbose) { - console.warn('😕 found translated node that should not be translated while English node should be translated', {enNode, translatedNode}); - console.trace(); - } - continue; - } - const translationsForLocale = translations.get(targetLanguage) ?? new Map(); - const serializedNode = - ts.isStringLiteral(translatedNode) || ts.isNoSubstitutionTemplateLiteral(translatedNode) - ? translatedNode.getText().slice(1, -1) - : this.templateExpressionToString(translatedNode); - translationsForLocale.set(translationKey, serializedNode); - translations.set(targetLanguage, translationsForLocale); - - // For complex template expressions, we need a way to look up the English span hash for each translated span hash, so we track those here - if (ts.isTemplateExpression(enNode) && ts.isTemplateExpression(translatedNode) && !this.isSimpleTemplateExpression(enNode)) { - for (let i = 0; i < enNode.templateSpans.length; i++) { - const enSpan = enNode.templateSpans[i]; - const translatedSpan = translatedNode.templateSpans[i]; - this.translatedSpanHashToEnglishSpanHash.set(hashStr(translatedSpan.expression.getText()), hashStr(enSpan.expression.getText())); - } - } - } - }); - } - for (const targetLanguage of this.targetLanguages) { - // Map of translations - const translationsForLocale = translations.get(targetLanguage) ?? new Map(); - // Extract strings to translate const stringsToTranslate = new Map(); this.extractStringsToTranslate(this.sourceFile, stringsToTranslate); // Translate all the strings in parallel (up to 8 at a time) + const translations = new Map(); const translationPromises = []; for (const [key, {text, context}] of stringsToTranslate) { - if (translationsForLocale.has(key)) { - // This means that the translation for this key was already parsed from an existing translation file, so we don't need to translate it with ChatGPT - continue; - } - const translationPromise = promisePool.add(() => this.translator.translate(targetLanguage, text, context).then((result) => translationsForLocale.set(key, result))); + const translationPromise = promisePool.add(() => this.translator.translate(targetLanguage, text, context).then((result) => translations.set(key, result))); translationPromises.push(translationPromise); } await Promise.allSettled(translationPromises); // Replace translated strings in the AST - const transformer = this.createTransformer(translationsForLocale); + const transformer = this.createTransformer(translations); const result = ts.transform(this.sourceFile, [transformer]); let transformedSourceFile = result.transformed.at(0) ?? this.sourceFile; // Ensure we always have a valid SourceFile result.dispose(); @@ -243,23 +138,10 @@ class TranslationGenerator { } } - /** - * Each translation file should have an object called translations that's later default-exported. This function takes in a root node, and finds the translations node. - */ - private findTranslationsNode(sourceFile: ts.SourceFile): ts.Node | null { - const defaultExport = TSCompilerUtils.findDefaultExport(sourceFile); - if (!defaultExport) { - throw new Error('Could not find default export in source file'); - } - const defaultExportIdentifier = TSCompilerUtils.extractIdentifierFromExpression(defaultExport); - const translationsNode = TSCompilerUtils.resolveDeclaration(defaultExportIdentifier ?? '', sourceFile); - return translationsNode; - } - /** * Should the given node be translated? */ - private shouldNodeBeTranslated(node: ts.Node): node is ts.StringLiteral | ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral { + private shouldNodeBeTranslated(node: ts.Node): boolean { // We only translate string literals and template expressions if (!ts.isStringLiteral(node) && !ts.isTemplateExpression(node) && !ts.isNoSubstitutionTemplateLiteral(node)) { return false; @@ -411,23 +293,20 @@ class TranslationGenerator { private extractStringsToTranslate(node: ts.Node, stringsToTranslate: Map) { if (this.shouldNodeBeTranslated(node)) { const context = this.getContextForNode(node); - const translationKey = this.getTranslationKey(node); // String literals and no-substitution templates can be translated directly if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { - stringsToTranslate.set(translationKey, {text: node.text, context}); + stringsToTranslate.set(this.getTranslationKey(node), {text: node.text, context}); } // Template expressions must be encoded directly before they can be translated else if (ts.isTemplateExpression(node)) { if (this.isSimpleTemplateExpression(node)) { - stringsToTranslate.set(translationKey, {text: this.templateExpressionToString(node), context}); + stringsToTranslate.set(this.getTranslationKey(node), {text: this.templateExpressionToString(node), context}); } else { - if (this.verbose) { - console.debug('😵‍💫 Encountered complex template, recursively translating its spans first:', node.getText()); - } + console.log('😵‍💫 Encountered complex template, recursively translating its spans first:', node.getText()); node.templateSpans.forEach((span) => this.extractStringsToTranslate(span, stringsToTranslate)); - stringsToTranslate.set(translationKey, {text: this.templateExpressionToString(node), context}); + stringsToTranslate.set(this.getTranslationKey(node), {text: this.templateExpressionToString(node), context}); } } } @@ -478,14 +357,7 @@ class TranslationGenerator { if (/^\d+$/.test(trimmed)) { // It's a hash reference to a complex span const hashed = Number(trimmed); - - // If the translated, serialized template expression came from an existing translation file, then the hash of the complex expression will be a hash of the translated expression. - // If the translated, serialized template expression came from ChatGPT, then the hash of the complex expression will be a hash of the English expression. - // Meanwhile, translatedComplexExpressions is keyed by English hashes, because it comes from createTransformer, which is parsing and transforming an English file. - // So when rebuilding the template expression from its serialized form, we first search for the translated expression assuming the expression is serialized with English hashes. - // If that fails, we look up the English expression hash associated with the translated expression hash, then look up the translated expression using the English hash. - const translatedExpression = translatedComplexExpressions.get(hashed) ?? translatedComplexExpressions.get(this.translatedSpanHashToEnglishSpanHash.get(hashed) ?? hashed); - + const translatedExpression = translatedComplexExpressions.get(hashed); if (!translatedExpression) { throw new Error(`No template found for hash: ${hashed}`); } @@ -565,15 +437,12 @@ class TranslationGenerator { * The main function mostly contains CLI and file I/O logic, while TS parsing and translation logic is encapsulated in TranslationGenerator. */ async function main(): Promise { - /* eslint-disable @typescript-eslint/naming-convention */ const cli = new CLI({ flags: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'dry-run': { description: 'If true, just do local mocked translations rather than making real requests to an AI translator.', }, - verbose: { - description: 'Should we print verbose logs?', - }, }, namedArgs: { // By default, generate translations for all supported languages. Can be overridden with the --locales flag @@ -592,14 +461,8 @@ async function main(): Promise { return validatedLocales; }, }, - 'compare-ref': { - description: - 'For incremental translations, this ref is the previous version of the codebase to compare to. Only strings that changed or had their context changed since this ref will be retranslated.', - default: '', - }, }, } as const); - /* eslint-enable @typescript-eslint/naming-convention */ let translator: Translator; if (cli.flags['dry-run']) { @@ -625,10 +488,7 @@ async function main(): Promise { languagesDir, sourceFile: enSourceFile, translator, - compareRef: cli.namedArgs['compare-ref'], - verbose: cli.flags.verbose, }); - await generator.generateTranslations(); } diff --git a/scripts/utils/CLI.ts b/scripts/utils/CLI.ts index 0c6340e4fc22c..f3c7112a17fdb 100644 --- a/scripts/utils/CLI.ts +++ b/scripts/utils/CLI.ts @@ -208,7 +208,7 @@ class CLI { // Validate that all required args are present, assign defaults where values are not parsed for (const [name, spec] of Object.entries(config.namedArgs ?? {})) { if (!(name in parsedNamedArgs)) { - if (spec.default !== undefined) { + if (spec.default) { parsedNamedArgs[name as keyof typeof parsedNamedArgs] = spec.default as ValueOf; } else { throw new Error(`Missing required named argument --${name}`); @@ -217,7 +217,7 @@ class CLI { } for (const spec of config.positionalArgs ?? []) { if (!(spec.name in parsedPositionalArgs)) { - if (spec.default !== undefined) { + if (spec.default) { parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = spec.default as ValueOf; } else { throw new Error(`Missing required positional argument --${spec.name}`); diff --git a/scripts/utils/TSCompilerUtils.ts b/scripts/utils/TSCompilerUtils.ts index 1c2d7302ec6a5..ed87839963401 100644 --- a/scripts/utils/TSCompilerUtils.ts +++ b/scripts/utils/TSCompilerUtils.ts @@ -29,188 +29,11 @@ function addImport(sourceFile: ts.SourceFile, identifierName: string, modulePath ); // Find the index of the last import declaration - let lastImportIndex = -1; - for (let i = sourceFile.statements.length - 1; i >= 0; i--) { - if (ts.isImportDeclaration(sourceFile.statements[i])) { - lastImportIndex = i; - break; - } - } + const lastImportIndex = sourceFile.statements.findLastIndex((statement) => ts.isImportDeclaration(statement)); const updatedStatements = ts.factory.createNodeArray([...sourceFile.statements.slice(0, lastImportIndex + 1), newImport, ...sourceFile.statements.slice(lastImportIndex + 1)]); return ts.factory.updateSourceFile(sourceFile, updatedStatements); } -/** - * This type is just a simple wrapper around a ts node with a label. - */ -type LabeledNode = { - label: K; - node: ts.Node; -}; - -/** - * Custom type for expressions that have both 'expression' and 'type' properties. - * This is useful for satisfies expressions and type assertions. - */ -type ExpressionWithType = ts.Node & { - expression: ts.Expression; - type: ts.TypeNode; -}; - -/** - * Walks a list of AST nodes in parallel and applies the visitor function at each set of corresponding nodes. - * Traverses only to the depth and breadth of the shortest subtree at each level. - * - * disclaimer: I don't know how this should/will work for ASTs that don't share a common structure. For now, that's undefined behavior. - */ -function traverseASTsInParallel(roots: Array>, visit: (nodes: Record) => void): void { - if (roots.length === 0) { - return; - } - - const nodeMap: Partial> = {}; - for (const {label, node} of roots) { - nodeMap[label] = node; - } - visit(nodeMap as Record); - - // Collect children per label - const childrenByLabel = new Map(); - let minChildren = Infinity; - - for (const {label, node} of roots) { - const children = node.getChildren(); - childrenByLabel.set(label, children); - if (children.length < minChildren) { - minChildren = children.length; - } - } - - // Traverse child nodes in parallel, stopping at the shortest list - for (let i = 0; i < minChildren; i++) { - const nextLevel: Array> = []; - for (const {label} of roots) { - const children = childrenByLabel.get(label) ?? []; - const child = children.at(i); - if (child) { - nextLevel.push({label, node: child}); - } - } - traverseASTsInParallel(nextLevel, visit); - } -} - -/** - * Finds the node that is exported as the default export. - * Returns null if not found. - */ -function findDefaultExport(sourceFile: ts.SourceFile): ts.Node | null { - for (const statement of sourceFile.statements) { - if (ts.isExportAssignment(statement) && !statement.isExportEquals) { - return statement.expression; - } - - if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) { - for (const element of statement.exportClause.elements) { - if (element.name.text === 'default') { - return element.name; - } - } - } - } - - return null; -} - -/** - * Resolves the identifier name to its declaration node within the source file. - */ -function resolveDeclaration(name: string, sourceFile: ts.SourceFile): ts.Node | null { - for (const statement of sourceFile.statements) { - if (ts.isVariableStatement(statement)) { - for (const decl of statement.declarationList.declarations) { - if (ts.isIdentifier(decl.name) && decl.name.text === name) { - return decl; - } - } - } - - if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) { - return statement; - } - - if (ts.isClassDeclaration(statement) && statement.name?.text === name) { - return statement; - } - } - - return null; -} - -/** - * Check if a node is an expression that has both 'expression' and 'type' properties. - * This is useful for satisfies expressions and type assertions. - */ -function isExpressionWithType(node: ts.Node): node is ExpressionWithType { - return 'expression' in node && 'type' in node && node.expression !== undefined && node.type !== undefined; -} - -/** - * Check if a node is a satisfies expression by examining its structure. - * This is more robust than checking SyntaxKind numbers which might vary between TS versions. - */ -function isSatisfiesExpression(node: ts.Node): node is ExpressionWithType { - // Check if the node text contains 'satisfies' and has the expected structure - const nodeText = node.getText(); - if (!nodeText.includes(' satisfies ')) { - return false; - } - - return isExpressionWithType(node); -} - -/** - * Extracts the identifier name from various expression types. - * Handles cases like: - * - Simple identifier: `translations` - * - Satisfies expression: `translations satisfies SomeType` - * - As expression: `translations as SomeType` - * - Parenthesized expression: `(translations)` - * - Type assertion: `translations` - */ -function extractIdentifierFromExpression(node: ts.Node): string | null { - // Direct identifier - if (ts.isIdentifier(node)) { - return node.text; - } - - // Check for satisfies expression by looking at the node structure - // A satisfies expression has the form: expression satisfies type - if (isSatisfiesExpression(node)) { - return extractIdentifierFromExpression(node.expression); - } - - // As expression: `translations as SomeType` - if (ts.isAsExpression(node)) { - return extractIdentifierFromExpression(node.expression); - } - - // Parenthesized expression: `(translations)` - if (ts.isParenthesizedExpression(node)) { - return extractIdentifierFromExpression(node.expression); - } - - // Type assertion: `translations` - // Check for type assertion by looking for angle bracket syntax and structure - const nodeText = node.getText(); - if (nodeText.includes('<') && nodeText.includes('>') && 'expression' in node && 'type' in node && node.expression !== undefined && node.type !== undefined) { - return extractIdentifierFromExpression(node.expression as ts.Node); - } - - return null; -} - -export default {findAncestor, addImport, traverseASTsInParallel, findDefaultExport, resolveDeclaration, extractIdentifierFromExpression}; -export type {LabeledNode, ExpressionWithType}; +export default {findAncestor, addImport}; diff --git a/tests/unit/TSCompilerUtilsTest.ts b/tests/unit/TSCompilerUtilsTest.ts index 0839208f66647..9f96cd811974b 100644 --- a/tests/unit/TSCompilerUtilsTest.ts +++ b/tests/unit/TSCompilerUtilsTest.ts @@ -101,351 +101,4 @@ describe('TSCompilerUtils', () => { ); }); }); - - describe('traverseASTsInParallel', () => { - it('visits all nodes in lockstep and applies individual visitors', () => { - const en = `const x = "Hello"; function greet(name: string) { return \`Hi \${name}\`; }`; - const it = `const x = "Ciao"; function greet(name: string) { return \`Ciao \${name}\`; }`; - - const enAST = createSourceFile(en); - const itAST = createSourceFile(it); - - const enKinds: ts.SyntaxKind[] = []; - const itKinds: ts.SyntaxKind[] = []; - - TSCompilerUtils.traverseASTsInParallel( - [ - {label: 'en', node: enAST}, - {label: 'it', node: itAST}, - ], - (nodes) => { - const enNode = nodes.en; - enKinds.push(enNode.kind); - const itNode = nodes.it; - itKinds.push(itNode.kind); - }, - ); - - expect(enKinds.length).toBe(itKinds.length); - for (let i = 0; i < enKinds.length; i++) { - expect(enKinds.at(i)).toBe(itKinds.at(i)); - } - }); - - it('collects matching string literals from multiple ASTs', () => { - const en = `const a = "Hello"; const b = \`World\`;`; - const it = `const a = "Ciao"; const b = \`Mondo\`;`; - - const enAST = createSourceFile(en); - const itAST = createSourceFile(it); - - const enStrings: string[] = []; - const itStrings: string[] = []; - - TSCompilerUtils.traverseASTsInParallel( - [ - {label: 'en', node: enAST}, - {label: 'it', node: itAST}, - ], - (nodes) => { - const enNode = nodes.en; - const itNode = nodes.it; - if (ts.isStringLiteral(enNode) || ts.isNoSubstitutionTemplateLiteral(enNode)) { - enStrings.push(enNode.text); - } - if (ts.isStringLiteral(itNode) || ts.isNoSubstitutionTemplateLiteral(itNode)) { - itStrings.push(itNode.text); - } - }, - ); - - expect(enStrings).toEqual(['Hello', 'World']); - expect(itStrings).toEqual(['Ciao', 'Mondo']); - }); - - it('traverses only the shared structure when node counts differ', () => { - const code1 = `const x = { a: 1, b: 2 };`; - const code2 = `const x = { a: 1 };`; - - const ast1 = createSourceFile(code1); - const ast2 = createSourceFile(code2); - - let count1 = 0; - let count2 = 0; - - TSCompilerUtils.traverseASTsInParallel( - [ - {label: 'one', node: ast1}, - {label: 'two', node: ast2}, - ], - (nodes) => { - if (nodes.one) { - count1++; - } - if (nodes.two) { - count2++; - } - }, - ); - - // Expect both to visit the same number of shared nodes - expect(count1).toBe(count2); - }); - - it('does nothing when given an empty array', () => { - expect(() => - TSCompilerUtils.traverseASTsInParallel([], () => { - throw new Error(); - }), - ).not.toThrow(); - }); - - it('handles nested objects', () => { - const ast1 = createSourceFile('const x = { a: 1, b: {c: 2}, d: 3};'); - const ast2 = createSourceFile('const x = { a: 1, b: {c: 2}, d: 3};'); - - TSCompilerUtils.traverseASTsInParallel( - [ - { - label: 'one', - node: ast1, - }, - { - label: 'two', - node: ast2, - }, - ], - (nodes) => { - expect(nodes.one).toStrictEqual(nodes.two); - }, - ); - }); - }); - - describe('findDefaultExport', () => { - it('returns the identifier in `export default` statement', () => { - const code = dedent(` - const strings = { greeting: 'Hello' }; - export default strings; - `); - const ast = createSourceFile(code); - const result = TSCompilerUtils.findDefaultExport(ast); - expect(result?.getText()).toBe('strings'); - }); - - it('returns the object literal if directly exported', () => { - const code = dedent(` - export default { farewell: 'Goodbye' }; - `); - const ast = createSourceFile(code); - const result = TSCompilerUtils.findDefaultExport(ast); - expect(result).not.toBeNull(); - if (!result) { - return; - } - expect(ts.isObjectLiteralExpression(result)).toBe(true); - expect(result?.getText()).toContain('farewell'); - }); - - it('returns null if no default export is present', () => { - const code = dedent(` - const foo = 'bar'; - export const greeting = 'Hello'; - `); - const ast = createSourceFile(code); - const result = TSCompilerUtils.findDefaultExport(ast); - expect(result).toBeNull(); - }); - - it('returns identifier for `export { foo as default }`', () => { - const code = dedent(` - const foo = { bar: 'baz' }; - export { foo as default }; - `); - const ast = createSourceFile(code); - const result = TSCompilerUtils.findDefaultExport(ast); - expect(result?.getText()).toBe('default'); - }); - }); - - describe('resolveDeclaration', () => { - it('resolves a variable declaration', () => { - const code = dedent(` - const foo = { message: 'hi' }; - `); - const ast = createSourceFile(code); - const node = TSCompilerUtils.resolveDeclaration('foo', ast); - - expect(node).not.toBeNull(); - if (!node) { - return; - } - expect(ts.isVariableDeclaration(node)).toBe(true); - expect(node.getText()).toContain('message'); - }); - - it('resolves a function declaration', () => { - const code = dedent(` - function greet() { - return 'hello'; - } - `); - const ast = createSourceFile(code); - const node = TSCompilerUtils.resolveDeclaration('greet', ast); - - expect(node).not.toBeNull(); - if (!node) { - return; - } - expect(ts.isFunctionDeclaration(node)).toBe(true); - expect(node.getText()).toContain('hello'); - }); - - it('resolves a class declaration', () => { - const code = dedent(` - class MyClass { - method() {} - } - `); - const ast = createSourceFile(code); - const node = TSCompilerUtils.resolveDeclaration('MyClass', ast); - - expect(node).not.toBeNull(); - if (!node) { - return; - } - expect(ts.isClassDeclaration(node)).toBe(true); - expect(node.getText()).toContain('method'); - }); - - it('returns null for unknown identifier', () => { - const code = dedent(` - const foo = 123; - `); - const ast = createSourceFile(code); - const node = TSCompilerUtils.resolveDeclaration('bar', ast); - expect(node).toBeNull(); - }); - - it('returns declaration even if variable has no initializer', () => { - const code = dedent(` - let foo; - `); - const ast = createSourceFile(code); - const node = TSCompilerUtils.resolveDeclaration('foo', ast); - - expect(node).not.toBeNull(); - if (!node) { - return; - } - expect(ts.isVariableDeclaration(node)).toBe(true); - }); - }); - - describe('extractIdentifierFromExpression', () => { - it('extracts identifier from simple identifier', () => { - const code = 'translations'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from satisfies expression', () => { - const code = 'translations satisfies TranslationDeepObject;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from as expression', () => { - const code = 'translations as SomeType;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from parenthesized expression', () => { - const code = '(translations);'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from nested parenthesized expression', () => { - const code = '((translations));'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from type assertion (angle bracket syntax)', () => { - const code = 'translations;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - // Note: This might be 'translations' or null depending on how TypeScript parses angle bracket syntax in JSX-enabled contexts - expect(result).toEqual(expect.any(String)); - }); - - it('extracts identifier from complex nested expression', () => { - const code = '(translations as SomeType);'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('extracts identifier from satisfies expression with nested parentheses', () => { - const code = '(translations) satisfies TranslationDeepObject;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - - it('returns null for non-identifier expressions', () => { - const code = '"hello world";'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBeNull(); - }); - - it('returns null for complex expressions that do not contain identifiers', () => { - const code = '42 + 24;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBeNull(); - }); - - it('returns null for call expressions', () => { - const code = 'someFunction();'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBeNull(); - }); - - it('returns null for member expressions', () => { - const code = 'obj.property;'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBeNull(); - }); - - it('handles deeply nested expression types', () => { - const code = '((translations as SomeType) satisfies AnotherType);'; - const ast = createSourceFile(code); - const expression = ast.statements[0] as ts.ExpressionStatement; - const result = TSCompilerUtils.extractIdentifierFromExpression(expression.expression); - expect(result).toBe('translations'); - }); - }); }); diff --git a/tests/unit/generateTranslationsTest.ts b/tests/unit/generateTranslationsTest.ts index a1f0661ae265b..eef3815745f31 100644 --- a/tests/unit/generateTranslationsTest.ts +++ b/tests/unit/generateTranslationsTest.ts @@ -4,10 +4,9 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import GitHubUtils from '@github/libs/GithubUtils'; import dedent from '@libs/StringUtils/dedent'; -import generateTranslations, {GENERATED_FILE_PREFIX} from '@scripts/generateTranslations'; -import Translator from '@scripts/utils/Translator/Translator'; +import generateTranslations, {GENERATED_FILE_PREFIX} from '../../scripts/generateTranslations'; +import Translator from '../../scripts/utils/Translator/Translator'; jest.mock('openai'); @@ -30,7 +29,7 @@ describe('generateTranslations', () => { process.env.LANGUAGES_DIR = LANGUAGES_DIR; // Set dry-run flag for tests - process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it']; + process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--locales', 'it']; }); afterEach(() => { @@ -448,117 +447,4 @@ describe('generateTranslations', () => { `)}`, ); }); - - it('reuses existing translations from --compare-ref', async () => { - // Step 1: simulate an old version with initial translations - const oldDir = fs.mkdtempSync(path.join(os.tmpdir(), 'translations-old-')); - const oldItPath = path.join(oldDir, 'src/languages/it.ts'); - const oldEnPath = path.join(oldDir, 'src/languages/en.ts'); - fs.mkdirSync(path.dirname(oldItPath), {recursive: true}); - - fs.writeFileSync( - oldEnPath, - dedent(` - const strings = { - greeting: 'Hello', - unchanged: 'Unchanged', - func: (name: string) => \`Hello \${name}\`, - noSubstitutionTemplate: \`Salutations\`, - complexFunc: (numScanning: number, numPending: number) => { - const statusText: string[] = []; - if (numScanning > 0) { - statusText.push(\`\${numScanning} scanning\`); - } - if (numPending > 0) { - statusText.push(\`\${numPending} pending\`); - } - return statusText.length > 0 ? \`1 expense (\${statusText.join(', ')})\` : '1 expense'; - }, - extraComplex: (payer: string) => \`\${payer ? \`\${payer} as payer \` : ''}paid elsewhere\`, - }; - export default strings; - `), - 'utf8', - ); - fs.writeFileSync( - oldItPath, - dedent(` - import type en from './en'; - const strings = { - greeting: '[it] Hello', - unchanged: '[it] Unchanged', - func: (name: string) => \`[it] Hello \${name}\`, - noSubstitutionTemplate: \`[it] Salutations\`, - complexFunc: (numScanning: number, numPending: number) => { - const statusText: string[] = []; - if (numScanning > 0) { - statusText.push(\`[it] \${numScanning} scanning\`); - } - if (numPending > 0) { - statusText.push(\`[it] \${numPending} pending\`); - } - return statusText.length > 0 ? \`[it] 1 expense (\${statusText.join(', ')})\` : '[it] 1 expense'; - }, - extraComplex: (payer: string) => \`[it] \${payer ? \`[it] \${payer} as payer \` : ''}paid elsewhere\`, - }; - export default strings; - `), - 'utf8', - ); - - // Step 2: patch GitHubUtils.getFileContents to load from disk - jest.spyOn(GitHubUtils, 'getFileContents').mockImplementation((filePath: string) => { - if (filePath.endsWith('en.ts')) { - return Promise.resolve(fs.readFileSync(oldEnPath, 'utf8')); - } - if (filePath.endsWith('it.ts')) { - return Promise.resolve(fs.readFileSync(oldItPath, 'utf8')); - } - throw new Error(`Unexpected filePath: ${filePath}`); - }); - - // Step 3: create new source with one changed and one unchanged string - fs.writeFileSync( - EN_PATH, - dedent(` - const strings = { - greeting: 'Hello', - unchanged: 'Unchanged', - func: (name: string) => \`Hello \${name}\`, - noSubstitutionTemplate: \`Salutations\`, - complexFunc: (numScanning: number, numPending: number) => { - const statusText: string[] = []; - if (numScanning > 0) { - statusText.push(\`\${numScanning} scanning\`); - } - if (numPending > 0) { - statusText.push(\`\${numPending} pending\`); - } - return statusText.length > 0 ? \`1 expense (\${statusText.join(', ')})\` : '1 expense'; - }, - extraComplex: (payer: string) => \`\${payer ? \`\${payer} as payer \` : ''}paid elsewhere\`, - newKey: 'New value!', - }; - export default strings; - `), - 'utf8', - ); - - process.argv.push('--compare-ref=ref-does-not-matter-due-to-mock'); - const translateSpy = jest.spyOn(Translator.prototype, 'translate'); - - await generateTranslations(); - const itContent = fs.readFileSync(IT_PATH, 'utf8'); - - expect(itContent).toContain('[it] Hello'); - expect(itContent).toContain('[it] Unchanged'); - // eslint-disable-next-line no-template-curly-in-string - expect(itContent).toContain('[it] Hello ${name}'); - expect(itContent).toContain('[it] Salutations'); - expect(itContent).toContain('[it] New value!'); - expect(translateSpy).toHaveBeenCalledTimes(1); - expect(translateSpy).toHaveBeenCalledWith('it', 'New value!', undefined); - - fs.rmSync(oldDir, {recursive: true}); - }); });