@tolgee/cli 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/{index.js → cli.js} +54 -63
  2. package/dist/client/ApiClient.js +72 -0
  3. package/dist/client/ExportClient.js +19 -0
  4. package/dist/client/ImportClient.js +22 -0
  5. package/dist/client/TolgeeClient.js +18 -0
  6. package/dist/client/errorFromLoadable.js +35 -0
  7. package/dist/client/getApiKeyInformation.js +39 -0
  8. package/dist/client/internal/requester.js +2 -5
  9. package/dist/commands/extract/check.js +10 -8
  10. package/dist/commands/extract/print.js +10 -8
  11. package/dist/commands/extract.js +3 -5
  12. package/dist/commands/login.js +6 -14
  13. package/dist/commands/pull.js +38 -32
  14. package/dist/commands/push.js +89 -71
  15. package/dist/commands/sync/compare.js +14 -11
  16. package/dist/commands/sync/sync.js +41 -24
  17. package/dist/commands/tag.js +49 -0
  18. package/dist/config/tolgeerc.js +59 -36
  19. package/dist/constants.js +0 -1
  20. package/dist/extractor/machines/vue/decoder.js +1 -1
  21. package/dist/options.js +31 -7
  22. package/dist/utils/checkPathNotAFile.js +16 -0
  23. package/dist/utils/getSingleOption.js +10 -0
  24. package/dist/utils/getStackTrace.js +7 -0
  25. package/dist/utils/logger.js +8 -0
  26. package/dist/utils/mapExportFormat.js +62 -0
  27. package/dist/utils/mapImportFormat.js +18 -0
  28. package/dist/utils/prepareDir.js +12 -0
  29. package/dist/utils/zip.js +2 -7
  30. package/package.json +17 -13
  31. package/schema.json +175 -0
  32. package/dist/arguments.js +0 -2
  33. package/dist/client/errors.js +0 -37
  34. package/dist/client/export.js +0 -20
  35. package/dist/client/import.js +0 -55
  36. package/dist/client/index.js +0 -73
  37. package/dist/client/languages.js +0 -13
  38. package/dist/client/project.js +0 -41
  39. package/dist/utils/overwriteDir.js +0 -34
@@ -1,9 +1,25 @@
1
- import { join } from 'path';
1
+ import { extname, join } from 'path';
2
2
  import { readdir, readFile, stat } from 'fs/promises';
3
3
  import { Command, Option } from 'commander';
4
- import { HttpError } from '../client/errors.js';
4
+ import { glob } from 'glob';
5
+ import { loading, success, error, warn, exitWithError, } from '../utils/logger.js';
5
6
  import { askString } from '../utils/ask.js';
6
- import { loading, success, warn, error } from '../utils/logger.js';
7
+ import { mapImportFormat } from '../utils/mapImportFormat.js';
8
+ import { handleLoadableError } from '../client/TolgeeClient.js';
9
+ async function allInPattern(pattern) {
10
+ const files = [];
11
+ const items = await glob(pattern);
12
+ for (const item of items) {
13
+ if ((await stat(item)).isDirectory()) {
14
+ files.push(...(await readDirectory(item)));
15
+ }
16
+ else {
17
+ const blob = await readFile(item);
18
+ files.push({ name: item, data: blob });
19
+ }
20
+ }
21
+ return files;
22
+ }
7
23
  async function readDirectory(directory, base = '') {
8
24
  const files = [];
9
25
  const dir = await readdir(directory);
@@ -21,100 +37,102 @@ async function readDirectory(directory, base = '') {
21
37
  }
22
38
  return files;
23
39
  }
24
- function getConflictingLanguages(result) {
25
- const conflicts = [];
26
- const languages = result.result?._embedded?.languages;
27
- if (languages) {
28
- for (const l of languages) {
29
- if (l.conflictCount) {
30
- conflicts.push(l.id);
31
- }
32
- }
33
- }
34
- return conflicts;
35
- }
36
40
  async function promptConflicts(opts) {
37
- const projectId = opts.client.getProjectId();
38
- const resolveUrl = new URL(`/projects/${projectId}/import`, opts.apiUrl).href;
39
- if (opts.forceMode === 'NO') {
40
- error(`There are conflicts in the import. You can resolve them and complete the import here: ${resolveUrl}.`);
41
- process.exit(1);
41
+ if (opts.forceMode === 'NO_FORCE') {
42
+ exitWithError(`There are conflicts in the import and the force mode is set to "NO_FORCE". Set it to "KEEP" or "OVERRIDE" to continue.`);
42
43
  }
43
44
  if (opts.forceMode) {
44
45
  return opts.forceMode;
45
46
  }
46
47
  if (!process.stdout.isTTY) {
47
- error(`There are conflicts in the import. Please specify a --force-mode, or resolve them in your browser at ${resolveUrl}.`);
48
- process.exit(1);
48
+ exitWithError(`There are conflicts in the import. Please specify a --force-mode.`);
49
49
  }
50
50
  warn('There are conflicts in the import. What do you want to do?');
51
51
  const resp = await askString('Type "KEEP" to preserve the version on the server, "OVERRIDE" to use the version from the import, and nothing to abort: ');
52
52
  if (resp !== 'KEEP' && resp !== 'OVERRIDE') {
53
- error(`Aborting. You can resolve the conflicts and complete the import here: ${resolveUrl}`);
54
- process.exit(1);
53
+ exitWithError(`Aborting.`);
55
54
  }
56
55
  return resp;
57
56
  }
58
- async function prepareImport(client, files) {
59
- return loading('Deleting import...', client.import.deleteImportIfExists()).then(() => loading('Uploading files...', client.import.addFiles({ files: files })));
60
- }
61
- async function resolveConflicts(client, locales, method) {
62
- for (const locale of locales) {
63
- if (method === 'KEEP') {
64
- await client.import.conflictsKeepExistingAll(locale);
65
- }
66
- else {
67
- await client.import.conflictsOverrideAll(locale);
68
- }
69
- }
57
+ async function importData(client, data) {
58
+ return loading('Uploading files...', client.import.import(data));
70
59
  }
71
- async function applyImport(client) {
72
- try {
73
- await loading('Applying changes...', client.import.applyImport());
74
- }
75
- catch (e) {
76
- if (e instanceof HttpError && e.response.statusCode === 400) {
77
- error("Some of the imported languages weren't recognized. Please create a language with corresponding tag in the Tolgee Platform.");
78
- return;
79
- }
80
- throw e;
60
+ async function readRecords(matchers) {
61
+ const result = [];
62
+ for (const matcher of matchers) {
63
+ const files = await allInPattern(matcher.path);
64
+ files.forEach((file) => {
65
+ result.push({
66
+ ...matcher,
67
+ data: file.data,
68
+ name: file.name,
69
+ });
70
+ });
81
71
  }
72
+ return result;
82
73
  }
83
- async function pushHandler(path) {
74
+ const pushHandler = (config) => async function () {
84
75
  const opts = this.optsWithGlobals();
85
- try {
86
- const stats = await stat(path);
87
- if (!stats.isDirectory()) {
88
- error('The specified path is not a directory.');
89
- process.exit(1);
90
- }
76
+ if (!config.push?.files) {
77
+ throw new Error('Missing option `push.files` in configuration file.');
91
78
  }
92
- catch (e) {
93
- if (e.code === 'ENOENT') {
94
- error('The specified path does not exist.');
95
- process.exit(1);
79
+ const filteredMatchers = config.push.files.filter((r) => {
80
+ if (opts.languages && !opts.languages.includes(r.language)) {
81
+ return false;
96
82
  }
97
- throw e;
98
- }
99
- const files = await loading('Reading files...', readDirectory(path));
83
+ if (opts.namespaces && !opts.namespaces.includes(r.namespace ?? '')) {
84
+ return false;
85
+ }
86
+ return true;
87
+ });
88
+ const files = await loading('Reading files...', readRecords(filteredMatchers));
100
89
  if (files.length === 0) {
101
90
  error('Nothing to import.');
102
91
  return;
103
92
  }
104
- const result = await prepareImport(opts.client, files);
105
- const conflicts = getConflictingLanguages(result);
106
- if (conflicts.length) {
107
- const resolveMethod = await promptConflicts(opts);
108
- await loading('Resolving conflicts...', resolveConflicts(opts.client, conflicts, resolveMethod));
93
+ const params = {
94
+ forceMode: opts.forceMode,
95
+ overrideKeyDescriptions: opts.overrideKeyDescriptions,
96
+ convertPlaceholdersToIcu: opts.convertPlaceholdersToIcu,
97
+ tagNewKeys: opts.tagNewKeys ?? [],
98
+ fileMappings: files.map((f) => {
99
+ const format = mapImportFormat(opts.format, extname(f.name));
100
+ return {
101
+ fileName: f.name,
102
+ format: format,
103
+ languageTag: f.language,
104
+ namespace: f.namespace ?? '',
105
+ };
106
+ }),
107
+ removeOtherKeys: opts.removeOtherKeys,
108
+ };
109
+ const attempt1 = await loading('Importing...', importData(opts.client, {
110
+ files,
111
+ params,
112
+ }));
113
+ if (attempt1.error) {
114
+ if (attempt1.error.code !== 'conflict_is_not_resolved') {
115
+ handleLoadableError(attempt1);
116
+ }
117
+ const forceMode = await promptConflicts(opts);
118
+ const attempt2 = await loading('Overriding...', importData(opts.client, {
119
+ files,
120
+ params: { ...params, forceMode },
121
+ }));
122
+ handleLoadableError(attempt2);
109
123
  }
110
- await applyImport(opts.client);
111
124
  success('Done!');
112
- }
113
- export default new Command()
125
+ };
126
+ export default (config) => new Command()
114
127
  .name('push')
115
128
  .description('Pushes translations to Tolgee')
116
- .argument('<path>', 'Path to the files to push to Tolgee')
117
129
  .addOption(new Option('-f, --force-mode <mode>', 'What should we do with possible conflicts? If unspecified, the user will be prompted interactively, or the command will fail when in non-interactive')
118
- .choices(['OVERRIDE', 'KEEP', 'NO'])
130
+ .choices(['OVERRIDE', 'KEEP', 'NO_FORCE'])
119
131
  .argParser((v) => v.toUpperCase()))
120
- .action(pushHandler);
132
+ .addOption(new Option('--override-key-descriptions', 'Override existing key descriptions from local files (only relevant for some formats).').default(config.push?.overrideKeyDescriptions ?? true))
133
+ .addOption(new Option('--convert-placeholders-to-icu', 'Convert placeholders in local files to ICU format.').default(config.push?.convertPlaceholdersToIcu ?? true))
134
+ .addOption(new Option('-l, --languages <languages...>', 'Specifies which languages should be pushed (see push.files in config).').default(config.push?.languages))
135
+ .addOption(new Option('-n, --namespaces <namespaces...>', 'Specifies which namespaces should be pushed (see push.files in config).').default(config.push?.namespaces))
136
+ .addOption(new Option('--tag-new-keys <tags...>', 'Specify tags that will be added to newly created keys.').default(config.push?.tagNewKeys))
137
+ .addOption(new Option('--remove-other-keys', 'Remove keys which are not present in the import.').default(false))
138
+ .action(pushHandler(config));
@@ -3,15 +3,20 @@ import ansi from 'ansi-colors';
3
3
  import { compareKeys, printKey } from './syncUtils.js';
4
4
  import { extractKeysOfFiles, filterExtractionResult, } from '../../extractor/runner.js';
5
5
  import { dumpWarnings } from '../../extractor/warnings.js';
6
- import { EXTRACTOR } from '../../options.js';
7
- import { loading } from '../../utils/logger.js';
8
- import { FILE_PATTERNS } from '../../arguments.js';
9
- async function compareHandler(filesPatterns) {
6
+ import { exitWithError, loading } from '../../utils/logger.js';
7
+ import { handleLoadableError } from '../../client/TolgeeClient.js';
8
+ const asyncHandler = (config) => async function () {
10
9
  const opts = this.optsWithGlobals();
11
- const rawKeys = await loading('Analyzing code...', extractKeysOfFiles(filesPatterns, opts.extractor));
10
+ const patterns = opts.patterns?.length ? opts.patterns : config.patterns;
11
+ if (!patterns?.length) {
12
+ exitWithError('Missing argument <patterns>');
13
+ }
14
+ const rawKeys = await loading('Analyzing code...', extractKeysOfFiles(patterns, opts.extractor));
12
15
  dumpWarnings(rawKeys);
13
16
  const localKeys = filterExtractionResult(rawKeys);
14
- const remoteKeys = await opts.client.project.fetchAllKeys();
17
+ const loadable = await opts.client.GET('/v2/projects/{projectId}/all-keys', { params: { path: { projectId: opts.client.getProjectId() } } });
18
+ handleLoadableError(loadable);
19
+ const remoteKeys = loadable.data._embedded.keys ?? [];
15
20
  const diff = compareKeys(localKeys, remoteKeys);
16
21
  if (!diff.added.length && !diff.removed.length) {
17
22
  console.log(ansi.green('Your code project is in sync with the associated Tolgee project!'));
@@ -37,10 +42,8 @@ async function compareHandler(filesPatterns) {
37
42
  console.log('');
38
43
  }
39
44
  console.log('Run `tolgee sync` to synchronize the projects.');
40
- }
41
- export default new Command()
45
+ };
46
+ export default (config) => new Command()
42
47
  .name('compare')
43
48
  .description('Compares the keys in your code project and in the Tolgee project.')
44
- .addArgument(FILE_PATTERNS)
45
- .addOption(EXTRACTOR)
46
- .action(compareHandler);
49
+ .action(asyncHandler(config));
@@ -3,59 +3,70 @@ import ansi from 'ansi-colors';
3
3
  import { extractKeysOfFiles, filterExtractionResult, } from '../../extractor/runner.js';
4
4
  import { dumpWarnings } from '../../extractor/warnings.js';
5
5
  import { compareKeys, printKey } from './syncUtils.js';
6
- import { overwriteDir } from '../../utils/overwriteDir.js';
6
+ import { prepareDir } from '../../utils/prepareDir.js';
7
7
  import { unzipBuffer } from '../../utils/zip.js';
8
8
  import { askBoolean } from '../../utils/ask.js';
9
- import { loading, error } from '../../utils/logger.js';
10
- import { EXTRACTOR } from '../../options.js';
11
- import { FILE_PATTERNS } from '../../arguments.js';
9
+ import { loading, exitWithError } from '../../utils/logger.js';
10
+ import { handleLoadableError, } from '../../client/TolgeeClient.js';
12
11
  async function backup(client, dest) {
13
- const blob = await client.export.export({
12
+ const loadable = await client.export.export({
14
13
  format: 'JSON',
14
+ supportArrays: false,
15
15
  filterState: ['UNTRANSLATED', 'TRANSLATED', 'REVIEWED'],
16
16
  structureDelimiter: '',
17
17
  });
18
+ handleLoadableError(loadable);
19
+ const blob = loadable.data;
18
20
  await unzipBuffer(blob, dest);
19
21
  }
20
22
  async function askForConfirmation(keys, operation) {
21
23
  if (!process.stdout.isTTY) {
22
- error('You must run this command interactively, or specify --yes to proceed.');
23
- process.exit(1);
24
+ exitWithError('You must run this command interactively, or specify --yes to proceed.');
24
25
  }
25
26
  const str = `The following keys will be ${operation}:`;
26
27
  console.log(operation === 'created' ? ansi.bold.green(str) : ansi.bold.red(str));
27
28
  keys.forEach((k) => printKey(k, operation === 'deleted'));
28
29
  const shouldContinue = await askBoolean('Does this look correct?', true);
29
30
  if (!shouldContinue) {
30
- error('Aborting.');
31
- process.exit(1);
31
+ exitWithError('Aborting.');
32
32
  }
33
33
  }
34
- async function syncHandler(filesPatterns) {
34
+ const syncHandler = (config) => async function () {
35
35
  const opts = this.optsWithGlobals();
36
- const rawKeys = await loading('Analyzing code...', extractKeysOfFiles(filesPatterns, opts.extractor));
36
+ const patterns = opts.patterns?.length ? opts.patterns : config.patterns;
37
+ if (!patterns?.length) {
38
+ exitWithError('Missing argument <patterns>');
39
+ }
40
+ const rawKeys = await loading('Analyzing code...', extractKeysOfFiles(patterns, opts.extractor));
37
41
  const warnCount = dumpWarnings(rawKeys);
38
42
  if (!opts.continueOnWarning && warnCount) {
39
43
  console.log(ansi.bold.red('Aborting as warnings have been emitted.'));
40
44
  process.exit(1);
41
45
  }
42
46
  const localKeys = filterExtractionResult(rawKeys);
43
- const remoteKeys = await opts.client.project.fetchAllKeys();
47
+ const allKeysLoadable = await opts.client.GET('/v2/projects/{projectId}/all-keys', {
48
+ params: { path: { projectId: opts.client.getProjectId() } },
49
+ });
50
+ handleLoadableError(allKeysLoadable);
51
+ const remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
44
52
  const diff = compareKeys(localKeys, remoteKeys);
45
53
  if (!diff.added.length && !diff.removed.length) {
46
54
  console.log(ansi.green('Your code project is in sync with the associated Tolgee project!'));
47
55
  process.exit(0);
48
56
  }
49
57
  // Load project settings. We're interested in the default locale here.
50
- const { baseLanguage } = await opts.client.project.fetchProjectInformation();
58
+ const projectLoadable = await opts.client.GET('/v2/projects/{projectId}', {
59
+ params: { path: { projectId: opts.client.getProjectId() } },
60
+ });
61
+ handleLoadableError(projectLoadable);
62
+ const baseLanguage = projectLoadable.data.baseLanguage;
51
63
  if (!baseLanguage) {
52
64
  // I'm highly unsure how we could reach this state, but this is what the OAI spec tells me ¯\_(ツ)_/¯
53
- error('Your project does not have a base language!');
54
- process.exit(1);
65
+ exitWithError('Your project does not have a base language!');
55
66
  }
56
67
  // Prepare backup
57
68
  if (opts.backup) {
58
- await overwriteDir(opts.backup, opts.yes);
69
+ await prepareDir(opts.backup, opts.yes);
59
70
  await loading('Backing up Tolgee project', backup(opts.client, opts.backup));
60
71
  }
61
72
  // Create new keys
@@ -70,7 +81,11 @@ async function syncHandler(filesPatterns) {
70
81
  ? { [baseLanguage.tag]: key.defaultValue }
71
82
  : {},
72
83
  }));
73
- await loading('Creating missing keys...', opts.client.project.createBulkKey(keys));
84
+ const loadable = await loading('Creating missing keys...', opts.client.POST('/v2/projects/{projectId}/keys/import', {
85
+ params: { path: { projectId: opts.client.getProjectId() } },
86
+ body: { keys },
87
+ }));
88
+ handleLoadableError(loadable);
74
89
  }
75
90
  if (opts.removeUnused) {
76
91
  // Delete unused keys.
@@ -79,7 +94,11 @@ async function syncHandler(filesPatterns) {
79
94
  await askForConfirmation(diff.removed, 'deleted');
80
95
  }
81
96
  const ids = await diff.removed.map((k) => k.id);
82
- await loading('Deleting unused keys...', opts.client.project.deleteBulkKeys(ids));
97
+ const loadable = await loading('Deleting unused keys...', opts.client.DELETE('/v2/projects/{projectId}/keys', {
98
+ params: { path: { projectId: opts.client.getProjectId() } },
99
+ body: { ids },
100
+ }));
101
+ handleLoadableError(loadable);
83
102
  }
84
103
  }
85
104
  console.log(ansi.bold.green('Sync complete!'));
@@ -93,14 +112,12 @@ async function syncHandler(filesPatterns) {
93
112
  if (opts.backup) {
94
113
  console.log(ansi.blueBright(`A backup of the project prior to the synchronization has been dumped in ${opts.backup}.`));
95
114
  }
96
- }
97
- export default new Command()
115
+ };
116
+ export default (config) => new Command()
98
117
  .name('sync')
99
118
  .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`.')
100
- .addArgument(FILE_PATTERNS)
101
- .addOption(EXTRACTOR)
102
- .option('-B, --backup <path>', '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.')
119
+ .option('-B, --backup <path>', 'Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state.')
103
120
  .option('--continue-on-warning', 'Set this flag to continue the sync if warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.')
104
121
  .option('-Y, --yes', 'Skip prompts and automatically say yes to them. You will not be asked for confirmation before creating/deleting keys.')
105
122
  .option('--remove-unused', 'Also delete unused keys from the Tolgee project.')
106
- .action(syncHandler);
123
+ .action(syncHandler(config));
@@ -0,0 +1,49 @@
1
+ import { Command, Option } from 'commander';
2
+ import { handleLoadableError } from '../client/TolgeeClient.js';
3
+ import { exitWithError, loading, success } from '../utils/logger.js';
4
+ import { extractKeysOfFiles } from '../extractor/runner.js';
5
+ const tagHandler = (config) => async function () {
6
+ const opts = this.optsWithGlobals();
7
+ let extractedKeys;
8
+ if (opts.filterExtracted || opts.filterNotExtracted) {
9
+ if (opts.filterExtracted && opts.filterNotExtracted) {
10
+ exitWithError('Use either "--filter-extracted" or "--filter-not-extracted", not both');
11
+ }
12
+ const patterns = opts.patterns;
13
+ if (!patterns?.length) {
14
+ exitWithError('Missing option --patterns or config.patterns option');
15
+ }
16
+ const extracted = await loading('Analyzing code...', extractKeysOfFiles(patterns, opts.extractor));
17
+ const keys = [...extracted.values()].flatMap((item) => item.keys);
18
+ extractedKeys = keys.map((key) => ({
19
+ name: key.keyName,
20
+ namespace: key.namespace,
21
+ }));
22
+ }
23
+ const loadable = await loading('Tagging...', opts.client.PUT('/v2/projects/{projectId}/tag-complex', {
24
+ params: { path: { projectId: opts.client.getProjectId() } },
25
+ body: {
26
+ filterTag: opts.filterTag,
27
+ filterTagNot: opts.filterNoTag,
28
+ tagFiltered: opts.tag,
29
+ tagOther: opts.tagOther,
30
+ untagFiltered: opts.untag,
31
+ untagOther: opts.untagOther,
32
+ filterKeys: opts.filterExtracted ? extractedKeys : undefined,
33
+ filterKeysNot: opts.filterNotExtracted ? extractedKeys : undefined,
34
+ },
35
+ }));
36
+ handleLoadableError(loadable);
37
+ success('Done!');
38
+ };
39
+ export default (config) => new Command('tag')
40
+ .description('Update tags in your project.')
41
+ .addOption(new Option('--filter-extracted', 'Extract keys from code and filter by it.'))
42
+ .addOption(new Option('--filter-not-extracted', 'Extract keys from code and filter them out.'))
43
+ .addOption(new Option('--filter-tag <tags...>', 'Filter only keys with tag. Use * as a wildcard.'))
44
+ .addOption(new Option('--filter-no-tag <tags...>', 'Filter only keys without tag. Use * as a wildcard.'))
45
+ .addOption(new Option('--tag <tags...>', 'Add tag to filtered keys.'))
46
+ .addOption(new Option('--tag-other <tags...>', 'Tag keys which are not filtered.'))
47
+ .addOption(new Option('--untag <tags...>', 'Remove tag from filtered keys. Use * as a wildcard.'))
48
+ .addOption(new Option('--untag-other <tags...>', 'Remove tag from keys which are not filtered. Use * as a wildcard.'))
49
+ .action(tagHandler(config));
@@ -1,61 +1,84 @@
1
1
  import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
2
- import { resolve } from 'path';
2
+ import { Validator } from 'jsonschema';
3
+ import { readFile } from 'fs/promises';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join, resolve } from 'path';
6
+ import { error, exitWithError } from '../utils/logger.js';
3
7
  import { existsSync } from 'fs';
4
- import { SDKS } from '../constants.js';
5
8
  const explorer = cosmiconfig('tolgee', {
6
9
  loaders: {
7
10
  noExt: defaultLoaders['.json'],
8
11
  },
9
12
  });
10
- function parseConfig(rc) {
11
- if (typeof rc !== 'object' || Array.isArray(rc)) {
12
- throw new Error('Invalid config: config is not an object.');
13
- }
14
- const cfg = {};
15
- if ('apiUrl' in rc) {
16
- if (typeof rc.apiUrl !== 'string') {
17
- throw new Error('Invalid config: apiUrl is not a string');
18
- }
13
+ function parseConfig(input, configDir) {
14
+ const rc = { ...input };
15
+ if (rc.apiUrl !== undefined) {
19
16
  try {
20
- cfg.apiUrl = new URL(rc.apiUrl);
17
+ new URL(rc.apiUrl);
21
18
  }
22
19
  catch (e) {
23
20
  throw new Error('Invalid config: apiUrl is an invalid URL');
24
21
  }
25
22
  }
26
- if ('projectId' in rc) {
27
- cfg.projectId = Number(rc.projectId); // Number("") returns 0
28
- if (!Number.isInteger(cfg.projectId) || cfg.projectId <= 0) {
23
+ if (rc.projectId !== undefined) {
24
+ const projectId = Number(rc.projectId); // Number("") returns 0
25
+ if (!Number.isInteger(projectId) || projectId <= 0) {
29
26
  throw new Error('Invalid config: projectId should be an integer representing your project Id');
30
27
  }
31
28
  }
32
- if ('sdk' in rc) {
33
- if (!SDKS.includes(rc.sdk)) {
34
- throw new Error(`Invalid config: invalid sdk. Must be one of: ${SDKS.join(' ')}`);
29
+ if (rc.extractor !== undefined) {
30
+ rc.extractor = resolve(configDir, rc.extractor);
31
+ if (!existsSync(rc.extractor)) {
32
+ throw new Error(`Invalid config: extractor points to a file that does not exists (${rc.extractor})`);
35
33
  }
36
- cfg.sdk = rc.sdk;
37
34
  }
38
- if ('extractor' in rc) {
39
- if (typeof rc.extractor !== 'string') {
40
- throw new Error('Invalid config: extractor is not a string');
35
+ if (rc.delimiter !== undefined) {
36
+ rc.delimiter = rc.delimiter || '';
37
+ }
38
+ // convert relative paths in config to absolute
39
+ // so it's always relative to config location
40
+ if (rc.push?.files) {
41
+ rc.push.files = rc.push.files.map((r) => ({
42
+ ...r,
43
+ path: resolve(configDir, r.path),
44
+ }));
45
+ }
46
+ if (rc.pull?.path !== undefined) {
47
+ rc.pull.path = resolve(configDir, rc.pull.path);
48
+ }
49
+ if (rc.patterns !== undefined) {
50
+ rc.patterns = rc.patterns.map((pattern) => resolve(configDir, pattern));
51
+ }
52
+ return rc;
53
+ }
54
+ async function getSchema() {
55
+ const path = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..', 'schema.json');
56
+ return JSON.parse((await readFile(path)).toString());
57
+ }
58
+ export default async function loadTolgeeRc(path) {
59
+ let res;
60
+ if (path) {
61
+ try {
62
+ res = await explorer.load(path);
41
63
  }
42
- const extractorPath = resolve(rc.extractor);
43
- if (!existsSync(extractorPath)) {
44
- throw new Error(`Invalid config: extractor points to a file that does not exists (${extractorPath})`);
64
+ catch (e) {
65
+ error(e.message);
66
+ throw new Error(`Can't open config file on path "${path}"`);
45
67
  }
46
- cfg.extractor = extractorPath;
47
68
  }
48
- if ('delimiter' in rc) {
49
- if (typeof rc.delimiter !== 'string' && rc.delimiter !== null) {
50
- throw new Error('Invalid config: delimiter is not a string');
51
- }
52
- cfg.delimiter = rc.delimiter || '';
69
+ else {
70
+ res = await explorer.search();
53
71
  }
54
- return cfg;
55
- }
56
- export default async function loadTolgeeRc() {
57
- const res = await explorer.search();
58
72
  if (!res || res.isEmpty)
59
73
  return null;
60
- return parseConfig(res.config);
74
+ const config = parseConfig(res.config, dirname(path || '.'));
75
+ const validator = new Validator();
76
+ const schema = await getSchema();
77
+ const result = validator.validate(config, schema);
78
+ if (result.errors.length) {
79
+ const { message, property } = result.errors[0];
80
+ const errMessage = `Tolgee config: '${property.replace('instance.', '')}' ${message}`;
81
+ exitWithError(errMessage);
82
+ }
83
+ return config;
61
84
  }
package/dist/constants.js CHANGED
@@ -8,4 +8,3 @@ export const USER_AGENT = `Tolgee-CLI/${VERSION} (+https://github.com/tolgee/tol
8
8
  export const DEFAULT_API_URL = new URL('https://app.tolgee.io');
9
9
  export const API_KEY_PAT_PREFIX = 'tgpat_';
10
10
  export const API_KEY_PAK_PREFIX = 'tgpak_';
11
- export const SDKS = ['react', 'vue', 'svelte'];
@@ -17,7 +17,7 @@ export default createMachine({
17
17
  setup: [],
18
18
  script: [],
19
19
  template: [],
20
- scriptSetupConsumed: false,
20
+ scriptSetupConsumed: false, // <script setup>
21
21
  invalidSetup: null,
22
22
  depth: 0,
23
23
  memoizedDepth: 0,
package/dist/options.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { Option, InvalidArgumentError } from 'commander';
4
- import { DEFAULT_API_URL } from './constants.js';
5
4
  function parseProjectId(v) {
6
5
  const val = Number(v);
7
6
  if (!Number.isInteger(val) || val < 1) {
@@ -25,10 +24,35 @@ function parsePath(v) {
25
24
  return path;
26
25
  }
27
26
  export const API_KEY_OPT = new Option('-ak, --api-key <key>', 'Tolgee API Key. Can be a Project API Key or a Personal Access Token.').env('TOLGEE_API_KEY');
28
- export const PROJECT_ID_OPT = new Option('-p, --project-id <id>', 'Project ID. Only required when using a Personal Access Token.')
29
- .default(-1)
30
- .argParser(parseProjectId);
31
- export const API_URL_OPT = new Option('-au, --api-url <url>', 'The url of Tolgee API.')
32
- .default(DEFAULT_API_URL)
33
- .argParser(parseUrlArgument);
27
+ export const PROJECT_ID_OPT = new Option('-p, --project-id <id>', 'Project ID. Only required when using a Personal Access Token.').argParser(parseProjectId);
28
+ export const API_URL_OPT = new Option('-au, --api-url <url>', 'The url of Tolgee API.').argParser(parseUrlArgument);
34
29
  export const EXTRACTOR = new Option('-e, --extractor <extractor>', `A path to a custom extractor to use instead of the default one.`).argParser(parsePath);
30
+ export const CONFIG_OPT = new Option('-c, --config [config]', 'A path to tolgeerc config file.').argParser(parsePath);
31
+ export const FORMAT_OPT = new Option('--format <format>', 'Localization files format.').choices([
32
+ 'JSON_TOLGEE',
33
+ 'JSON_ICU',
34
+ 'JSON_JAVA',
35
+ 'JSON_PHP',
36
+ 'JSON_RUBY',
37
+ 'JSON_C',
38
+ 'PO_PHP',
39
+ 'PO_C',
40
+ 'PO_JAVA',
41
+ 'PO_ICU',
42
+ 'PO_RUBY',
43
+ 'APPLE_STRINGS',
44
+ 'APPLE_XLIFF',
45
+ 'PROPERTIES_ICU',
46
+ 'PROPERTIES_JAVA',
47
+ 'ANDROID_XML',
48
+ 'FLUTTER_ARB',
49
+ 'YAML_RUBY',
50
+ 'YAML_JAVA',
51
+ 'YAML_ICU',
52
+ 'YAML_PHP',
53
+ 'XLIFF_ICU',
54
+ 'XLIFF_JAVA',
55
+ 'XLIFF_PHP',
56
+ 'XLIFF_RUBY',
57
+ ]);
58
+ export const FILE_PATTERNS = new Option('-pt, --patterns <patterns...>', 'File glob patterns to include (hint: make sure to escape it in quotes, or your shell might attempt to unroll some tokens like *)');
@@ -0,0 +1,16 @@
1
+ import { stat } from 'fs/promises';
2
+ import { exitWithError } from './logger.js';
3
+ export async function checkPathNotAFile(path) {
4
+ try {
5
+ const stats = await stat(path);
6
+ if (!stats.isDirectory()) {
7
+ exitWithError('The specified path already exists and is not a directory.');
8
+ }
9
+ }
10
+ catch (e) {
11
+ // Ignore "file doesn't exist" error
12
+ if (e.code !== 'ENOENT') {
13
+ throw e;
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Get single option from arguments, without parsing the rest
4
+ */
5
+ export function getSingleOption(option, args) {
6
+ const findOne = new Command();
7
+ findOne.allowUnknownOption().helpOption(false).addOption(option);
8
+ findOne.parse(args);
9
+ return findOne.opts()[option.name()];
10
+ }
@@ -0,0 +1,7 @@
1
+ export const getStackTrace = () => {
2
+ const obj = {};
3
+ Error.captureStackTrace(obj, getStackTrace);
4
+ const stack = obj.stack;
5
+ const parts = stack.split('\n');
6
+ return parts.slice(2).join('\n');
7
+ };