From 2eb8f7a721aba1a6afb89e5d5f7cde2fe1cad9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Tue, 23 Apr 2024 12:26:57 +0200 Subject: [PATCH] fix: improve config logic, and attach config properties to options explicitly --- package-lock.json | 16 +++---- package.json | 2 +- schema.json | 87 ++++++++++++++++++++++++++--------- src/commands/extract.ts | 2 +- src/commands/extract/check.ts | 2 +- src/commands/extract/print.ts | 2 +- src/commands/pull.ts | 17 +++---- src/commands/push.ts | 13 ++++-- src/commands/sync/compare.ts | 12 ++--- src/commands/sync/sync.ts | 4 +- src/config/tolgeerc.ts | 19 +++++--- src/index.ts | 46 +++++++++--------- src/schema.d.ts | 71 ++++++++++++++++++++-------- 13 files changed, 186 insertions(+), 107 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99ed0cbf..2fcdbe4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "ansi-colors": "^4.1.3", "base32-decode": "^1.0.0", - "commander": "^11.0.0", + "commander": "^12.0.0", "cosmiconfig": "^8.2.0", "form-data": "^4.0.0", "glob": "^10.3.3", @@ -3120,11 +3120,11 @@ } }, "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/compare-func": { @@ -14949,9 +14949,9 @@ } }, "commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==" + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==" }, "compare-func": { "version": "2.0.0", diff --git a/package.json b/package.json index d1aba7a7..626fe6bf 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "ansi-colors": "^4.1.3", "base32-decode": "^1.0.0", - "commander": "^11.0.0", + "commander": "^12.0.0", "cosmiconfig": "^8.2.0", "form-data": "^4.0.0", "glob": "^10.3.3", diff --git a/schema.json b/schema.json index 5355333a..2fed93aa 100644 --- a/schema.json +++ b/schema.json @@ -18,44 +18,85 @@ "type": "string" }, "patterns": { - "description": "File glob patterns to include", + "description": "File glob patterns to your source code, used for keys extraction.", "type": "array", "items": { "type": "string" } }, - "path": { - "description": "Path to folder with localization files for push/pull commands.", - "type": "string" - }, "format": { - "description": "Format of the exported files.", - "type": "string" + "description": "Format for push and pull operations.", + "enum": [ + "JSON", + "JSON_TOLGEE", + "XLIFF", + "PO", + "APPLE_STRINGS_STRINGSDICT", + "APPLE_XLIFF", + "ANDROID_XML", + "FLUTTER_ARB", + "PROPERTIES", + "YAML_RUBY", + "YAML" + ] }, - "languages": { - "description": "List of languages to pull. Leave unspecified to export them all.", - "type": "array", - "items": { - "type": "string" + "push": { + "type": "object", + "properties": { + "path": { + "description": "File glob specifying which files to include.", + "type": "string" + }, + "records": { "type": "array", "items": { "$ref": "#/$defs/record" } } } }, - "states": { - "description": "List of translation states to include. Defaults all except untranslated.", - "type": "array", - "items": { - "enum": ["UNTRANSLATED", "TRANSLATED", "REVIEWED"] + "pull": { + "type": "object", + "properties": { + "path": { + "description": "File ", + "type": "string" + }, + "languages": { + "description": "List of languages to pull. Leave unspecified to export them all.", + "type": "array", + "items": { + "type": "string" + } + }, + "states": { + "description": "List of translation states to include. Defaults all except untranslated.", + "type": "array", + "items": { + "enum": ["UNTRANSLATED", "TRANSLATED", "REVIEWED"] + } + }, + "namespaces": { + "description": "List of namespaces to pull. Defaults to all namespaces.", + "type": "array", + "items": { + "type": "string" + } + } } }, "delimiter": { "description": "Structure delimiter to use. By default, Tolgee interprets `.` as a nested structure. You can change the delimiter, or disable structure formatting by not specifying any value to the option.", "type": ["string", "null"] - }, - "namespaces": { - "description": "List of namespaces to pull. Defaults to all namespaces.", - "type": "array", - "items": { - "type": "string" + } + }, + "$defs": { + "record": { + "type": "object", + "properties": { + "path": { "$ref": "#/$defs/path" }, + "language": { "type": "string" }, + "namespace": { "type": "string" } } + }, + "path": { + "description": "File glob specifying which files to include.", + "type": "string" } } } diff --git a/src/commands/extract.ts b/src/commands/extract.ts index 4b693a65..0b7fba1d 100644 --- a/src/commands/extract.ts +++ b/src/commands/extract.ts @@ -12,6 +12,6 @@ export type BaseExtractOptions = { export default (config: Schema) => new Command('extract') .description('Extracts strings from your projects') - .addOption(EXTRACTOR) + .addOption(EXTRACTOR.default(config.extractor)) .addCommand(extractPrint(config)) .addCommand(extractCheck(config)); diff --git a/src/commands/extract/check.ts b/src/commands/extract/check.ts index c79b750e..05ab821d 100644 --- a/src/commands/extract/check.ts +++ b/src/commands/extract/check.ts @@ -70,5 +70,5 @@ export default (config: Schema) => .description( 'Checks if the keys can be extracted automatically, and reports problems if any' ) - .addArgument(FILE_PATTERNS) + .addArgument(FILE_PATTERNS.default(config.patterns)) .action(lintHandler(config)); diff --git a/src/commands/extract/print.ts b/src/commands/extract/print.ts index 0ee4228f..027971b5 100644 --- a/src/commands/extract/print.ts +++ b/src/commands/extract/print.ts @@ -78,5 +78,5 @@ const printHandler = (config: Schema) => export default (config: Schema) => new Command('print') .description('Prints extracted data to the console') - .addArgument(FILE_PATTERNS) + .addArgument(FILE_PATTERNS.default(config.patterns)) .action(printHandler(config)); diff --git a/src/commands/pull.ts b/src/commands/pull.ts index 2602bcdb..d09f5f90 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -28,10 +28,9 @@ async function fetchZipBlob(opts: PullOptions): Promise { }); } -const pullHandler = (config: Schema) => - async function (this: Command, argPath: string | undefined) { +const pullHandler = () => + async function (this: Command, path: string | undefined) { const opts: PullOptions = this.optsWithGlobals(); - const path = argPath ?? config.path; if (!path) { throw new Error('Missing or argument '); @@ -63,12 +62,13 @@ export default (config: Schema) => .description('Pulls translations to Tolgee') .argument( '[path]', - 'Destination path where translation files will be stored in' + 'Destination path where translation files will be stored in', + config.pull?.path ) .addOption( new Option('-f, --format ', 'Format of the exported files') .choices(['JSON', 'XLIFF']) - .default('JSON') + .default(config.format ?? 'JSON') .argParser((v) => v.toUpperCase()) ) .option( @@ -80,6 +80,7 @@ export default (config: Schema) => '-s, --states ', 'List of translation states to include. Defaults all except untranslated' ) + .default(config.pull?.languages) .choices(['UNTRANSLATED', 'TRANSLATED', 'REVIEWED']) .argParser((v, a: string[]) => [v.toUpperCase(), ...(a || [])]) ) @@ -88,17 +89,17 @@ export default (config: Schema) => '-d, --delimiter', 'Structure delimiter to use. By default, Tolgee interprets `.` as a nested structure. You can change the delimiter, or disable structure formatting by not specifying any value to the option' ) - .default('.') + .default(config.delimiter ?? '.') .argParser((v) => v || '') ) .addOption( new Option( '-n, --namespaces ', 'List of namespaces to pull. Defaults to all namespaces' - ) + ).default(config.pull?.namespaces) ) .option( '-o, --overwrite', 'Whether to automatically overwrite existing files. BE CAREFUL, THIS WILL WIPE *ALL* THE CONTENTS OF THE TARGET FOLDER. If unspecified, the user will be prompted interactively, or the command will fail when in non-interactive' ) - .action(pullHandler(config)); + .action(pullHandler()); diff --git a/src/commands/push.ts b/src/commands/push.ts index 52f3aa8b..e0513c39 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -126,10 +126,9 @@ async function applyImport(client: Client) { } } -const pushHandler = (config: Schema) => - async function (this: Command, pathArg: string) { +const pushHandler = () => + async function (this: Command, path: string) { const opts: PushOptions = this.optsWithGlobals(); - const path = pathArg ?? config.path; if (!path) { throw new Error('Missing or argument '); @@ -174,7 +173,11 @@ export default (config: Schema) => new Command() .name('push') .description('Pushes translations to Tolgee') - .argument('[path]', 'Path to the files to push to Tolgee') + .argument( + '[path]', + 'Path to the files to push to Tolgee', + config.push?.path + ) .addOption( new Option( '-f, --force-mode ', @@ -183,4 +186,4 @@ export default (config: Schema) => .choices(['OVERRIDE', 'KEEP', 'NO']) .argParser((v) => v.toUpperCase()) ) - .action(pushHandler(config)); + .action(pushHandler()); diff --git a/src/commands/sync/compare.ts b/src/commands/sync/compare.ts index aae89bb3..6896ebc6 100644 --- a/src/commands/sync/compare.ts +++ b/src/commands/sync/compare.ts @@ -17,12 +17,10 @@ type Options = BaseOptions & { extractor: string; }; -const asyncHandler = (config: Schema) => - async function (this: Command, filesPatterns: string[]) { +const asyncHandler = () => + async function (this: Command, patterns: string[]) { const opts: Options = this.optsWithGlobals(); - const patterns = filesPatterns.length ? filesPatterns : config.patterns; - if (!patterns?.length) { error('Missing argument '); process.exit(1); @@ -81,6 +79,6 @@ export default (config: Schema) => .description( 'Compares the keys in your code project and in the Tolgee project.' ) - .addArgument(FILE_PATTERNS) - .addOption(EXTRACTOR) - .action(asyncHandler(config)); + .addArgument(FILE_PATTERNS.default(config.patterns)) + .addOption(EXTRACTOR.default(config.extractor)) + .action(asyncHandler()); diff --git a/src/commands/sync/sync.ts b/src/commands/sync/sync.ts index 96eba244..3a68075d 100644 --- a/src/commands/sync/sync.ts +++ b/src/commands/sync/sync.ts @@ -188,8 +188,8 @@ export default (config: Schema) => .description( 'Synchronizes the keys in your code project and in the Tolgee project, by creating missing keys and optionally deleting unused ones. For a dry-run, use `tolgee compare`.' ) - .addArgument(FILE_PATTERNS) - .addOption(EXTRACTOR) + .addArgument(FILE_PATTERNS.default(config.patterns)) + .addOption(EXTRACTOR.default(config.extractor)) .option( '-B, --backup ', 'Path where a backup should be downloaded before performing the sync. If something goes wrong, the backup can be used to restore the project to its previous state.' diff --git a/src/config/tolgeerc.ts b/src/config/tolgeerc.ts index 63d1ed61..104758a6 100644 --- a/src/config/tolgeerc.ts +++ b/src/config/tolgeerc.ts @@ -44,8 +44,19 @@ function parseConfig(input: Schema, configDir: string): Schema { } } - if (rc.path !== undefined) { - rc.path = resolve(configDir, rc.path); + if (rc.delimiter !== undefined) { + rc.delimiter = rc.delimiter || ''; + } + + // convert relative paths in config to absolute + // so it's always relative to config location + + if (rc.push?.path !== undefined) { + rc.push.path = resolve(configDir, rc.push.path); + } + + if (rc.pull?.path !== undefined) { + rc.pull.path = resolve(configDir, rc.pull.path); } if (rc.patterns !== undefined) { @@ -54,10 +65,6 @@ function parseConfig(input: Schema, configDir: string): Schema { ); } - if (rc.delimiter !== undefined) { - rc.delimiter = rc.delimiter || ''; - } - return rc; } diff --git a/src/index.ts b/src/index.ts index 7ad48cf5..ae6252d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -142,28 +142,8 @@ const program = new Command('tolgee') // get config path to update defaults const configPath = getSingleOption(CONFIG_OPT, process.argv); -// Global options -program.addOption(CONFIG_OPT); -program.addOption(API_URL_OPT); -program.addOption(API_KEY_OPT); -program.addOption(PROJECT_ID_OPT); - async function loadConfig() { const tgConfig = await loadTolgeeRc(configPath); - if (tgConfig) { - [program, ...program.commands].forEach((cmd) => - cmd.options.forEach((opt) => { - const key = opt.attributeName(); - const value = (tgConfig as any)[key]; - if (value) { - const parsedValue = opt.parseArg - ? opt.parseArg(value, undefined) - : value; - cmd.setOptionValueWithSource(key, parsedValue, 'config'); - } - }) - ); - } return tgConfig ?? {}; } @@ -197,14 +177,30 @@ async function run() { try { const config = await loadConfig(); + // Global options + program.addOption(CONFIG_OPT); + program.addOption(API_URL_OPT.default(config.apiUrl)); + program.addOption(API_KEY_OPT.default(config.apiKey)); + program.addOption(PROJECT_ID_OPT.default(config.projectId)); + // Register commands program.addCommand(Login); program.addCommand(Logout); - program.addCommand(PushCommand(config)); - program.addCommand(PullCommand(config)); - program.addCommand(ExtractCommand(config)); - program.addCommand(CompareCommand(config)); - program.addCommand(SyncCommand(config)); + program.addCommand( + PushCommand(config).configureHelp({ showGlobalOptions: true }) + ); + program.addCommand( + PullCommand(config).configureHelp({ showGlobalOptions: true }) + ); + program.addCommand( + ExtractCommand(config).configureHelp({ showGlobalOptions: true }) + ); + program.addCommand( + CompareCommand(config).configureHelp({ showGlobalOptions: true }) + ); + program.addCommand( + SyncCommand(config).configureHelp({ showGlobalOptions: true }) + ); await program.parseAsync(); } catch (e: any) { diff --git a/src/schema.d.ts b/src/schema.d.ts index 1f924313..ab40e27e 100644 --- a/src/schema.d.ts +++ b/src/schema.d.ts @@ -5,6 +5,11 @@ * and run json-schema-to-typescript to regenerate this file. */ +/** + * File glob specifying which files to include. + */ +export type Path = string; + export interface Schema { /** * The url of Tolgee API. @@ -23,32 +28,60 @@ export interface Schema { */ extractor?: string; /** - * File glob patterns to include + * File glob patterns to your source code, used for keys extraction. */ patterns?: string[]; /** - * Path to folder with localization files for push/pull commands. - */ - path?: string; - /** - * Format of the exported files. - */ - format?: string; - /** - * List of languages to pull. Leave unspecified to export them all. + * Format for push and pull operations. */ - languages?: string[]; - /** - * List of translation states to include. Defaults all except untranslated. - */ - states?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED")[]; + format?: + | "JSON" + | "JSON_TOLGEE" + | "XLIFF" + | "PO" + | "APPLE_STRINGS_STRINGSDICT" + | "APPLE_XLIFF" + | "ANDROID_XML" + | "FLUTTER_ARB" + | "PROPERTIES" + | "YAML_RUBY" + | "YAML"; + push?: { + /** + * File glob specifying which files to include. + */ + path?: string; + records?: Record[]; + [k: string]: unknown; + }; + pull?: { + /** + * File + */ + path?: string; + /** + * List of languages to pull. Leave unspecified to export them all. + */ + languages?: string[]; + /** + * List of translation states to include. Defaults all except untranslated. + */ + states?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED")[]; + /** + * List of namespaces to pull. Defaults to all namespaces. + */ + namespaces?: string[]; + [k: string]: unknown; + }; /** * Structure delimiter to use. By default, Tolgee interprets `.` as a nested structure. You can change the delimiter, or disable structure formatting by not specifying any value to the option. */ delimiter?: string | null; - /** - * List of namespaces to pull. Defaults to all namespaces. - */ - namespaces?: string[]; + [k: string]: unknown; +} +export interface Record { + path?: Path; + language?: string; + namespace?: string; [k: string]: unknown; }