diff --git a/.DS_Store b/.DS_Store index 406f5c3f..1cf87f69 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/helper/download.ts b/src/helper/download.ts index 063d9e36..63c3aed6 100644 --- a/src/helper/download.ts +++ b/src/helper/download.ts @@ -10,7 +10,7 @@ export async function downloadFile(url: string, filePath: string) { response.data.pipe(writer); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); diff --git a/src/helper/locales.ts b/src/helper/locales.ts new file mode 100644 index 00000000..b27fe106 --- /dev/null +++ b/src/helper/locales.ts @@ -0,0 +1,278 @@ +import axios, { AxiosResponse } from 'axios'; +import { getActiveContext } from '../helper/utils'; +import { promises as fs } from 'fs'; +import fs_sync from 'fs'; +import * as path from 'path'; +import LocalesService from '../lib/api/services/locales.service'; + +/** + * Defines the synchronization mode for locale operations + * @enum {string} + */ +export enum SyncMode { + /** Pull changes from remote to local */ + PULL = 'pull', + /** Push changes from local to remote */ + PUSH = 'push' +} + +interface ApiConfig { + baseUrl: string; + headers: Record; +} + +interface LocaleResource { + locale: string; + resource: Record; + type: string; +} + +export const hasAnyDeltaBetweenLocalAndRemoteLocales = async (): Promise => { + let comparedFiles = 0; // Track the number of files compared + try { + console.log('Starting comparison between local and remote data'); + + const localesFolder: string = path.resolve(process.cwd(), 'theme/locales'); + + if (!fs_sync.existsSync(localesFolder)) { + console.log('Locales folder does not exist'); + return false; + } + + + // Fetch remote data from the API + const response: AxiosResponse = await LocalesService.getLocalesByThemeId(null); + + console.log('Response received from API:', response.status); + + if (response.status === 200) { + const data = response.data; // Extract the data from the API response + console.log('Remote data retrieved:', data); + + // Ensure the locales folder exists + await fs.mkdir(localesFolder, { recursive: true }); + console.log('Locales folder ensured at:', localesFolder); + + // Read all files in the local locales folder + const localFiles = await fs.readdir(localesFolder); + console.log('Local files found:', localFiles); + + // Compare each local file with the corresponding remote resource + for (const file of localFiles) { + let locale = path.basename(file, '.json'); // Extract the locale name from the file name + console.log('Processing local file:', file); + const localeType = locale.includes('schema') ? 'locale_schema' : 'locale'; + locale = locale.includes('schema') ? locale.replace('.schema', '') : locale; + const localData = JSON.parse(await fs.readFile(path.join(localesFolder, file), 'utf-8')); // Read and parse the local file + const matchingItem = data.items.find((item: LocaleResource) => item.locale === locale && item.type === localeType); // Find the corresponding remote item + + if (!matchingItem) { // If no matching remote item exists + console.log('No matching remote item found for locale:', locale); + return true; // Changes detected + } + + if (JSON.stringify(localData) !== JSON.stringify(matchingItem.resource)) { // Compare the local and remote data + console.log(`Data mismatch found for locale: ${locale}, Type: ${localeType}`); + return true; // Changes detected + } + + comparedFiles++; // Increment compared file count + } + + // Compare each remote resource with the corresponding local file + for (const item of data.items) { + const localeFile = path.join(localesFolder, `${item.locale}${item.type === 'locale_schema' ? '.schema' : ''}.json`);; // Construct the local file path + console.log('Processing remote item for locale file:', localeFile); + + const localeData = item.resource; // Extract the remote resource data + let currentData = {}; + + try { + // Attempt to read and parse the local file + currentData = JSON.parse(await fs.readFile(localeFile, 'utf-8')); + } catch (error) { + console.log('Error reading local file or file not found for locale:', item.locale, error); + currentData = {}; // Default to an empty object if the file is missing or invalid + } + + if (JSON.stringify(currentData) !== JSON.stringify(localeData)) { // Compare the local and remote data + console.log(`Data mismatch found for remote locale: ${item.locale}, Type: ${item?.type}`); + return true; // Changes detected + } + + comparedFiles++; // Increment compared file count + } + } else { + console.error(`Unexpected status code: ${response.status}.`); // Handle unexpected response status codes + return false; + } + } catch (error) { + console.error('Error checking for changes:', error); // Log errors during the comparison process + return false; + } + + console.log(`Comparison completed. Total files compared: ${comparedFiles}`); // Log the summary of comparisons + console.log('No changes detected between local and remote data'); // Log when no changes are detected + return false; // Return false if no changes were found +}; + +export const syncLocales = async (syncMode: SyncMode): Promise => { + + console.log(`Starting fetchData with SyncMode=${syncMode}`); + + try { + const response: AxiosResponse = await LocalesService.getLocalesByThemeId(null); + + console.log('API response received'); + + if (response.status === 200) { + const data = response.data; + console.log(`Fetched ${data.items.length} items from API`); + + const localesFolder: string = path.resolve(process.cwd(),'theme/locales'); + + try { + await fs.mkdir(localesFolder, { recursive: true }); + console.log('Ensured locales folder exists'); + } catch (err) { + console.error(`Error ensuring locales folder exists: ${(err as Error).message}`); + return; + } + + const unmatchedLocales: LocaleResource[] = []; + + let localFiles: string[]; + try { + localFiles = await fs.readdir(localesFolder); + console.log(`Found ${localFiles.length} local files`); + } catch (err) { + console.error(`Error reading locales folder: ${(err as Error).message}`); + return; + } + + for (const file of localFiles) { + let locale: string = path.basename(file, '.json'); + console.log(`Processing local file: ${locale}`); + const localeType = locale.includes('schema') ? 'locale_schema' : 'locale'; + locale = locale.includes('schema') ? locale.replace('.schema', '') : locale; + let localData: Record; + try { + localData = JSON.parse(await fs.readFile(path.join(localesFolder, file), 'utf-8')); + } catch (err) { + console.error(`Error reading file ${file}: ${(err as Error).message}`); + continue; + } + const matchingItem = data.items.find((item: LocaleResource) => item.locale === locale && item.type === localeType); + if (!matchingItem) { + console.log(`No matching item found for locale: ${locale}`); + unmatchedLocales.push({ locale, resource: localData, type: localeType }); + if (syncMode === SyncMode.PUSH) { + console.log(`Creating new resource in API for locale: ${locale}`); + await createLocaleInAPI(localData, locale, localeType); + } + } else { + if (syncMode === SyncMode.PUSH) { + if (JSON.stringify(localData) !== JSON.stringify(matchingItem.resource)) { + console.log(`Updating API resource for locale: ${locale}`); + await updateLocaleInAPI(localData, matchingItem._id); + } else { + console.log(`No changes detected for API resource: ${locale}`); + } + } else { + if (JSON.stringify(localData) !== JSON.stringify(matchingItem.resource)) { + console.log(`Updating local file for locale: ${locale}`); + await updateLocaleFile(path.join(localesFolder, file), matchingItem.resource, matchingItem._id); + } else { + console.log(`No changes detected for local file: ${locale}`); + } + } + } + } + + if (unmatchedLocales.length > 0) { + console.log('Unmatched locales:', unmatchedLocales); + } + + if (syncMode === SyncMode.PULL) { + for (const item of data.items) { + const locale: string = item.locale; + const localeFile = path.join(localesFolder, `${item.locale}${item.type === 'locale_schema' ? '.schema' : ''}.json`); + const localeData: Record = item.resource; + if (!localeData) { + console.log(`Skipping empty resource for locale: ${locale}`); + continue; + } + + let currentData: Record; + try { + currentData = JSON.parse(await fs.readFile(localeFile, 'utf-8').catch(() => '{}')); + } catch (err) { + console.error(`Error reading file ${localeFile}: ${(err as Error).message}`); + continue; + } + + if (JSON.stringify(currentData) !== JSON.stringify(localeData)) { + try { + console.log(`Writing updated data to local file: ${localeFile}`); + await fs.writeFile(localeFile, JSON.stringify(localeData, null, 2), 'utf-8'); + } catch (err) { + console.error(`Error writing to file ${localeFile}: ${(err as Error).message}`); + } + } else { + console.log(`No changes detected for local file: ${locale}`); + } + } + } + + console.log('Sync completed successfully.'); + } else { + console.error(`Unexpected status code: ${response.status}.`); + } + } catch (error) { + if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') { + console.error('Error: The request timed out. Please try again later.'); + } else { + console.error(`Error fetching data: ${error?.response?.status} - ${error?.response?.statusText || error?.message}`); + } + } +}; + +const createLocaleInAPI = async (data: Record, locale: string, localeType: string): Promise => { + try { + console.log(`Creating resource in API for locale: ${locale}`); + const activeContext = getActiveContext(); + const response: AxiosResponse = await LocalesService.createLocale(null, { + theme_id: activeContext.theme_id, + locale: locale, + resource: data, + type: localeType, + template: false, + }) + console.log('Locale created in API:', response.data); + } catch (error) { + console.log(error); + console.error('Error creating locale in API:', (error as Error).message); + } +}; + +const updateLocaleInAPI = async (data: Record, id: string): Promise => { + try { + console.log(`Updating resource in API for ID: ${id}`); + + const response: AxiosResponse = await LocalesService.updateLocale(null, id, { resource: data }); + + console.log('Locale updated in API:', response.data); + } catch (error) { + console.log(error); + console.error('Error updating locale in API:', (error as Error).message); + } +}; + +const updateLocaleFile = async (filePath: string, data: Record, id: string): Promise => { + try { + await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); + console.log(`Locale file updated: ${filePath}`); + } catch (err) { + console.error(`Error writing to file ${filePath}: ${(err as Error).message}`); + } +}; \ No newline at end of file diff --git a/src/helper/serve.utils.ts b/src/helper/serve.utils.ts index f6df962b..607547d4 100644 --- a/src/helper/serve.utils.ts +++ b/src/helper/serve.utils.ts @@ -27,6 +27,7 @@ import CommandError from '../lib/CommandError'; import Debug from '../lib/Debug'; import { SupportedFrameworks } from '../lib/ExtensionSection'; import https from 'https'; +import Tunnel from '../lib/Tunnel'; const packageJSON = require('../../package.json'); const BUILD_FOLDER = './.fdk/dist'; @@ -360,9 +361,32 @@ export async function startServer({ domain, host, isSSR, port }) { }); } + async function startTunnel(port: number) { + + try { + const tunnelInstance = new Tunnel({ + port, + }) + + const tunnelUrl = await tunnelInstance.startTunnel(); + + console.info(` + Started cloudflare tunnel at ${port}: ${tunnelUrl}`) + return { + url: tunnelUrl, + port, + }; + } catch (error) { + Logger.error('Error during starting cloudflare tunnel: ' + error.message); + return; + } +} + export async function startReactServer({ domain, host, isHMREnabled, port }) { const { currentContext, app, server, io } = await setupServer({ domain }); + const { url } = await startTunnel(port); + if (isHMREnabled) { let webpackConfigFromTheme = {}; const themeWebpackConfigPath = path.join( @@ -421,6 +445,29 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) { const uploadedFiles = {}; + app.get('/getAllStaticResources', (req, res) => { + const locale = req.query.locale || 'en'; + const localesFolder: string = path.resolve(process.cwd(), 'theme/locales'); + const locales = fs.readdirSync(localesFolder).filter(file => file.split('.')[0] === locale); + const localesArray = []; + + // Read content of each locale file + locales.forEach(locale => { + const filePath = path.join(localesFolder, locale); + try { + const content = fs.readFileSync(filePath, 'utf8'); + localesArray.push({ + "locale":locale.replace('.json', ''), + "resource":JSON.parse(content) + }); + } catch (error) { + Logger.error(`Error reading locale file ${locale}: ${error.message}`); + } + }); + + res.json({"items":localesArray}); + }); + app.get('/*', async (req, res) => { try { // If browser is not requesting for html page (it can be file, API call, etc...), then fetch and send requested data directly from source @@ -490,10 +537,14 @@ export async function startReactServer({ domain, host, isHMREnabled, port }) { cliMeta: { port, domain: getFullLocalUrl(port), + tunnelUrl: url, }, }, { - headers, + headers: { + ...headers, + cookie: req.headers.cookie, + }, }, ) .catch((error) => { diff --git a/src/lib/Theme.ts b/src/lib/Theme.ts index d151e3c3..bbc2626e 100644 --- a/src/lib/Theme.ts +++ b/src/lib/Theme.ts @@ -27,6 +27,7 @@ import glob from 'glob'; import _ from 'lodash'; import React from 'react'; import * as ReactRouterDOM from 'react-router-dom'; +import { syncLocales, hasAnyDeltaBetweenLocalAndRemoteLocales, SyncMode } from '../helper/locales' import { createDirectory, writeFile, readFile } from '../helper/file.utils'; import { customAlphabet } from 'nanoid'; @@ -1297,6 +1298,43 @@ export default class Theme { throw new CommandError(error.message, error.code); } }; + + public static syncRemoteToLocal = async (theme) => { + try { + const newConfig = Theme.getSettingsData(theme); + await Theme.writeSettingJson( + Theme.getSettingsDataPath(), + newConfig, + ); + await syncLocales(SyncMode.PULL); + Logger.info('Remote to Local: Config updated successfully'); + } catch (error) { + throw new CommandError(error.message, error.code); + } + } + + public static syncLocalToRemote = async (theme) => { + try { + const { data: theme } = await ThemeService.getThemeById(null); + await syncLocales(SyncMode.PUSH); + Logger.info('Locale to Remote: Config updated successfully'); + } catch (error) { + throw new CommandError(error.message, error.code); + } + } + + public static isAnyDeltaBetweenLocalAndRemote = async (theme, isNew) => { + const newConfig = Theme.getSettingsData(theme); + const oldConfig = await Theme.readSettingsJson( + Theme.getSettingsDataPath(), + ); + const isLocalAndRemoteLocalesChanged = await hasAnyDeltaBetweenLocalAndRemoteLocales(); + console.log('Locales changed: ', isLocalAndRemoteLocalesChanged); + const themeConfigChanged = (!isNew && !_.isEqual(newConfig, oldConfig)); + console.log('Theme config changed: ', themeConfigChanged); + return themeConfigChanged || isLocalAndRemoteLocalesChanged; + } + public static pullThemeConfig = async () => { try { const { data: theme } = await ThemeService.getThemeById(null); @@ -1311,6 +1349,7 @@ export default class Theme { Theme.getSettingsDataPath(), newConfig, ); + await Theme.syncRemoteToLocal(theme); Theme.createVueConfig(); Logger.info('Config updated successfully'); } catch (error) { @@ -2564,6 +2603,16 @@ export default class Theme { } }); } + + if (await Theme.isAnyDeltaBetweenLocalAndRemote(theme, isNew)) { + await inquirer.prompt(questions).then(async (answers) => { + if (answers.pullConfig) { + await Theme.syncRemoteToLocal(theme); + } else { + await Theme.syncLocalToRemote(theme); + } + }); + } } catch (err) { throw new CommandError(err.message, err.code); } diff --git a/src/lib/api/services/locales.service.ts b/src/lib/api/services/locales.service.ts new file mode 100644 index 00000000..ac135b21 --- /dev/null +++ b/src/lib/api/services/locales.service.ts @@ -0,0 +1,66 @@ +import { getActiveContext } from '../../../helper/utils'; +import ApiClient from '../ApiClient'; +import { URLS } from './url'; +import { getCommonHeaderOptions } from './utils'; + + +export default { + getLocalesByThemeId: async (data) => { + try { + const activeContext = data ? data : getActiveContext(); + const axiosOption = Object.assign({}, getCommonHeaderOptions()); + const res = await ApiClient.get( + URLS.GET_LOCALES( + activeContext.application_id, + activeContext.company_id, + activeContext.theme_id, + ), + axiosOption, + ); + return res; + } catch (error) { + throw error; + } + }, + createLocale: async (data, requestBody) => { + try { + const activeContext = data ? data : getActiveContext(); + const axiosOption = Object.assign({}, + { + data: requestBody + }, + getCommonHeaderOptions()); + const res = await ApiClient.post( + URLS.CREATE_LOCALE( + activeContext.application_id, + activeContext.company_id + ), + axiosOption, + ); + return res; + } catch (error) { + throw error; + } + }, + updateLocale: async (data, resource_id, requestBody) => { + try { + const activeContext = data ? data : getActiveContext(); + const axiosOption = Object.assign({}, + { + data: requestBody, + }, + getCommonHeaderOptions()); + const res = await ApiClient.put( + URLS.UPDATE_LOCALE( + activeContext.application_id, + activeContext.company_id, + resource_id + ), + axiosOption, + ); + return res; + } catch (error) { + throw error; + } + } +} \ No newline at end of file diff --git a/src/lib/api/services/url.ts b/src/lib/api/services/url.ts index b6a91204..a21169b2 100644 --- a/src/lib/api/services/url.ts +++ b/src/lib/api/services/url.ts @@ -28,6 +28,8 @@ const MIXMASTER_URL = (serverType: string) => getBaseURL() + `/service/${serverType}/partners/v` + apiVersion; const ASSET_URL = () => getBaseURL() + '/service/partner/assets/v2.0'; +const LOCALES_URL = () => getBaseURL() + '/service/partner/content/v' + apiVersion; + export const URLS = { // AUTHENTICATION LOGIN_USER: () => { @@ -232,4 +234,39 @@ export const URLS = { `/organization/${getOrganizationId()}/accounts/access-request?page_size=${page_size}&page_no=${page_no}&request_status=accepted`, ); }, + + //Locales + GET_LOCALES: ( + application_id: string, + company_id: number, + theme_id: string, + ) => { + console.log(urlJoin( + LOCALES_URL(), + `organization/${getOrganizationId()}/company/${company_id}/application/${application_id}/translate-ui-labels?theme_id=${theme_id}&page_size=500`, + )) + return urlJoin( + LOCALES_URL(), + `organization/${getOrganizationId()}/company/${company_id}/application/${application_id}/translate-ui-labels?theme_id=${theme_id}&page_size=500`, + ); + }, + CREATE_LOCALE: ( + application_id: string, + company_id: number + ) => { + return urlJoin( + LOCALES_URL(), + `organization/${getOrganizationId()}/company/${company_id}/application/${application_id}/translate-ui-labels`, + ); + }, + UPDATE_LOCALE: ( + application_id: string, + company_id: number, + resource_id: string + ) => { + return urlJoin( + LOCALES_URL(), + `organization/${getOrganizationId()}/company/${company_id}/application/${application_id}/translate-ui-labels/${resource_id}`, + ); + } };