@logto/cli 1.5.0 → 1.7.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.
package/LICENSE CHANGED
@@ -1,9 +1,3 @@
1
- Portions of this software are licensed as follows:
2
-
3
- * All content that resides under the "packages/cloud" directory of this repository, if that directory exists, is licensed under the license defined in "packages/cloud/LICENSE" (Elastic-2.0).
4
- * All third party components incorporated into this software are licensed under the original license provided by the owner of the applicable component.
5
- * Content outside of the above mentioned directories or restrictions above is available under the "MPL-2.0" license as defined below.
6
-
7
1
  Mozilla Public License Version 2.0
8
2
  ==================================
9
3
 
@@ -109,9 +109,9 @@ export const addConnectorsToPath = async (cwd, packageNames) => {
109
109
  throw new Error(`Unable to execute ${chalk.green('npm pack')} on package ${chalk.green(packageName)}`);
110
110
  }
111
111
  const { filename, name, version } = result[0];
112
- const escapedFilename = filename.replace(/\//g, '-').replace(/@/g, '');
112
+ const escapedFilename = filename.replaceAll('/', '-').replaceAll('@', '');
113
113
  const tarPath = path.join(cwd, escapedFilename);
114
- const packageDirectory = path.join(cwd, name.replace(/\//g, '-'));
114
+ const packageDirectory = path.join(cwd, name.replaceAll('/', '-'));
115
115
  await fs.rm(packageDirectory, { force: true, recursive: true });
116
116
  await fs.mkdir(packageDirectory, { recursive: true });
117
117
  await tar.extract({ cwd: packageDirectory, file: tarPath, strip: 1 });
@@ -13,7 +13,7 @@ const importAlterationScript = async (filePath) => {
13
13
  };
14
14
  export const getLatestAlterationTimestamp = async () => {
15
15
  const files = await getAlterationFiles();
16
- const lastFile = files[files.length - 1];
16
+ const lastFile = files.at(-1);
17
17
  if (!lastFile) {
18
18
  return 0;
19
19
  }
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, CloudScope, } from '@logto/schemas';
3
+ import { createDefaultAdminConsoleConfig, createCloudConnectionConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, CloudScope, } from '@logto/schemas';
4
4
  import { Tenants } from '@logto/schemas/models';
5
5
  import { sql } from 'slonik';
6
6
  import { raw } from 'slonik-sql-tag-raw';
@@ -96,6 +96,7 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
96
96
  await assignScopesToRole(connection, adminTenantId, adminAdminData.role.id, ...cloudAdditionalScopes.map(({ id }) => id));
97
97
  await Promise.all([
98
98
  connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
99
+ connection.query(insertInto(createCloudConnectionConfig(defaultTenantId, defaultTenantApplication.id, defaultTenantApplication.secret), 'logto_configs')),
99
100
  connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
100
101
  connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
101
102
  connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
@@ -8,7 +8,11 @@ import { getDatabaseName } from '../../../queries/database.js';
8
8
  export const createTenant = async (pool, tenantId) => {
9
9
  const database = await getDatabaseName(pool, true);
10
10
  const { parentRole, role, password } = createTenantMetadata(database, tenantId);
11
- const createTenant = { id: tenantId, dbUser: role, dbUserPassword: password };
11
+ const createTenant = {
12
+ id: tenantId,
13
+ dbUser: role,
14
+ dbUserPassword: password,
15
+ };
12
16
  await pool.query(insertInto(createTenant, 'tenants'));
13
17
  await pool.query(sql `
14
18
  create role ${sql.identifier([role])} with inherit login
@@ -61,7 +61,7 @@ export const validateDatabase = async () => {
61
61
  const pgOutput = safeExecSync('postgres --version') ?? '';
62
62
  // Filter out all brackets in the output since Homebrew will append `(Homebrew)`.
63
63
  const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('('));
64
- const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]);
64
+ const pgCurrent = semver.coerce(pgArray.at(-1));
65
65
  return !pgCurrent || pgCurrent.compare(pgRequired) < 0;
66
66
  },
67
67
  });
@@ -112,7 +112,7 @@ export const decompress = async (toPath, tarPath) => {
112
112
  export const seedDatabase = async (instancePath, cloud) => {
113
113
  try {
114
114
  const pool = await createPoolAndDatabaseIfNeeded();
115
- await seedByPool(pool);
115
+ await seedByPool(pool, cloud);
116
116
  await pool.end();
117
117
  }
118
118
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { noop } from '@silverhand/essentials';
2
2
  import create from './create.js';
3
3
  import listTags from './list-tags.js';
4
+ import syncKeys from './sync-keys/index.js';
4
5
  import sync from './sync.js';
5
6
  const translate = {
6
7
  command: ['translate', 't'],
@@ -14,6 +15,7 @@ const translate = {
14
15
  .command(create)
15
16
  .command(listTags)
16
17
  .command(sync)
18
+ .command(syncKeys)
17
19
  .demandCommand(1),
18
20
  handler: noop,
19
21
  };
@@ -4,7 +4,7 @@ import { got, HTTPError } from 'got';
4
4
  import { HttpsProxyAgent } from 'hpagent';
5
5
  import { z } from 'zod';
6
6
  import { consoleLog, getProxy } from '../../utils.js';
7
- import { getTranslationPrompt } from './prompts.js';
7
+ import { getTranslationPromptMessages } from './prompts.js';
8
8
  export const createOpenaiApi = () => {
9
9
  const proxy = getProxy();
10
10
  return got.extend({
@@ -30,16 +30,11 @@ export const translate = async ({ api, targetLanguage, sourceFilePath, extraProm
30
30
  .post('chat/completions', {
31
31
  json: {
32
32
  model: 'gpt-3.5-turbo',
33
- messages: [
34
- {
35
- role: 'user',
36
- content: getTranslationPrompt({
37
- sourceFileContent,
38
- targetLanguage,
39
- extraPrompt,
40
- }),
41
- },
42
- ],
33
+ messages: getTranslationPromptMessages({
34
+ sourceFileContent,
35
+ targetLanguage,
36
+ extraPrompt,
37
+ }),
43
38
  },
44
39
  })
45
40
  .json(), (error) => {
@@ -10,5 +10,8 @@ type GetTranslationPromptProperties = {
10
10
  * Remember to check the token limit before adding more prompt.
11
11
  * Tokens can be counted in https://platform.openai.com/tokenizer
12
12
  */
13
- export declare const getTranslationPrompt: ({ sourceFileContent, targetLanguage, extraPrompt, }: GetTranslationPromptProperties) => string;
13
+ export declare const getTranslationPromptMessages: ({ sourceFileContent, targetLanguage, extraPrompt, }: GetTranslationPromptProperties) => {
14
+ role: string;
15
+ content: string;
16
+ }[];
14
17
  export {};
@@ -6,13 +6,12 @@ import { conditionalString } from '@silverhand/essentials';
6
6
  * Remember to check the token limit before adding more prompt.
7
7
  * Tokens can be counted in https://platform.openai.com/tokenizer
8
8
  */
9
- export const getTranslationPrompt = ({ sourceFileContent, targetLanguage, extraPrompt, }) => `Given the following code snippet:
10
- \`\`\`ts
11
- ${sourceFileContent}
12
- \`\`\`
13
- only translate object values to ${languages[targetLanguage]}, keep all object keys original, output ts code only, the code format should be strictly consistent, and should not contains the given code snippet.
9
+ export const getTranslationPromptMessages = ({ sourceFileContent, targetLanguage, extraPrompt, }) => [
10
+ {
11
+ role: 'assistant',
12
+ content: `You are a translate assistant of a Typescript engineer, when you receive a code snippet that contains an object, translate and ONLY translate those values that are marked with comment "// UNTRANSLATED" into the language ${languages[targetLanguage]}, remove the "// UNTRANSLATED" mark, keep all object keys original, output ts code only, the code format should be strictly consistent, and should not contain the given code snippet. ${conditionalString(extraPrompt)}
14
13
 
15
- Take zh-cn as an example, if the input is:
14
+ Take Chinese as an example, if the input is:
16
15
  \`\`\`ts
17
16
  import others from './others.js';
18
17
 
@@ -35,5 +34,12 @@ const translation = {
35
34
  };
36
35
 
37
36
  export default translation;
38
- \`\`\`
39
- ${conditionalString(extraPrompt)}`;
37
+ \`\`\``,
38
+ },
39
+ {
40
+ role: 'user',
41
+ content: `\`\`\`ts
42
+ ${sourceFileContent}
43
+ \`\`\``,
44
+ },
45
+ ];
@@ -0,0 +1,11 @@
1
+ import { type CommandModule } from 'yargs';
2
+ declare const syncKeys: CommandModule<{
3
+ path?: string;
4
+ }, {
5
+ path?: string;
6
+ baseline: string;
7
+ target: string;
8
+ skipLint?: boolean;
9
+ package: string;
10
+ }>;
11
+ export default syncKeys;
@@ -0,0 +1,84 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { isLanguageTag } from '@logto/language-kit';
6
+ import ora from 'ora';
7
+ import { consoleLog } from '../../../utils.js';
8
+ import { inquireInstancePath } from '../../connector/utils.js';
9
+ import { praseLocaleFiles, syncPhraseKeysAndFileStructure } from './utils.js';
10
+ const execPromise = promisify(execFile);
11
+ const syncKeys = {
12
+ command: ['sync-keys', 'sk'],
13
+ describe: [
14
+ 'Sync nested object keys and the file structure from baseline to target.',
15
+ 'If a key is missing in the target, it will be added with a comment to indicate that the phrase is untranslated;',
16
+ 'If a key is missing in the baseline, it will be removed from the target;',
17
+ 'If a key exists in both the baseline and the target, the value of the target will be used.',
18
+ ].join(' '),
19
+ builder: (yargs) => yargs
20
+ .option('baseline', {
21
+ alias: 'b',
22
+ type: 'string',
23
+ describe: 'The baseline language tag',
24
+ default: 'en',
25
+ })
26
+ .option('package', {
27
+ alias: 'pkg',
28
+ type: 'string',
29
+ describe: 'The package name of the phrases, one of `phrases` or `phrases-ui`',
30
+ default: 'phrases',
31
+ })
32
+ .option('target', {
33
+ alias: 't',
34
+ type: 'string',
35
+ describe: 'The target language tag, or `all` to sync all languages',
36
+ })
37
+ .option('skip-lint', {
38
+ alias: 's',
39
+ type: 'boolean',
40
+ describe: 'Skip running `eslint --fix` for locales after syncing',
41
+ })
42
+ .demandOption(['baseline', 'target']),
43
+ handler: async ({ path: inputPath, baseline: baselineTag, target: targetTag, skipLint, package: packageName, }) => {
44
+ if (!isLanguageTag(baselineTag)) {
45
+ consoleLog.fatal('Invalid baseline language tag');
46
+ }
47
+ if (targetTag !== 'all' && !isLanguageTag(targetTag)) {
48
+ consoleLog.fatal('Invalid target language tag');
49
+ }
50
+ if (baselineTag === targetTag) {
51
+ consoleLog.fatal('Baseline and target cannot be the same');
52
+ }
53
+ if (packageName !== 'phrases' && packageName !== 'phrases-ui') {
54
+ consoleLog.fatal('Invalid package name, expected `phrases` or `phrases-ui`');
55
+ }
56
+ const instancePath = await inquireInstancePath(inputPath);
57
+ const phrasesPath = path.join(instancePath, 'packages', packageName);
58
+ const localesPath = path.join(phrasesPath, 'src/locales');
59
+ const entrypoint = path.join(localesPath, baselineTag.toLowerCase(), 'index.ts');
60
+ const baseline = praseLocaleFiles(entrypoint);
61
+ const targetLocales = targetTag === 'all' ? fs.readdirSync(localesPath) : [targetTag.toLowerCase()];
62
+ /* eslint-disable no-await-in-loop */
63
+ for (const target of targetLocales) {
64
+ if (target === baselineTag) {
65
+ continue;
66
+ }
67
+ const spinner = ora({
68
+ text: `Syncing object keys and file structure from ${baselineTag} to ${target}`,
69
+ }).start();
70
+ const targetDirectory = path.join(localesPath, target);
71
+ await syncPhraseKeysAndFileStructure(baseline, target, targetDirectory);
72
+ spinner.succeed(`Synced object keys and file structure from ${baselineTag} to ${target}`);
73
+ }
74
+ /* eslint-enable no-await-in-loop */
75
+ if (!skipLint) {
76
+ const spinner = ora({
77
+ text: 'Running `eslint --fix` for locales',
78
+ }).start();
79
+ await execPromise('pnpm', ['eslint', '--ext', '.ts', path.relative(phrasesPath, localesPath), '--fix'], { cwd: phrasesPath });
80
+ spinner.succeed('Ran `eslint --fix` for locales');
81
+ }
82
+ },
83
+ };
84
+ export default syncKeys;
@@ -0,0 +1,56 @@
1
+ type FileStructure = {
2
+ [key: string]: {
3
+ filePath?: string;
4
+ structure: FileStructure;
5
+ };
6
+ };
7
+ type NestedPhraseObject = {
8
+ [key: string]: string | NestedPhraseObject;
9
+ };
10
+ type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
11
+ /**
12
+ * Given a entrypoint file path of a language, parse the nested object of
13
+ * phrases and the file structure.
14
+ *
15
+ * @example
16
+ * Given the following file:
17
+ *
18
+ * ```ts
19
+ * import errors from './errors/index.js';
20
+ *
21
+ * const translation = {
22
+ * page_title: 'Anwendungen',
23
+ * errors,
24
+ * };
25
+ * ```
26
+ *
27
+ * The returned object will be:
28
+ *
29
+ * ```ts
30
+ * {
31
+ * page_title: 'Anwendungen',
32
+ * errors: {
33
+ * page_not_found: 'Seite nicht gefunden',
34
+ * },
35
+ * }
36
+ * ```
37
+ *
38
+ * And the file structure will be:
39
+ *
40
+ * ```ts
41
+ * {
42
+ * errors: {
43
+ * filePath: './errors/index.js',
44
+ * structure: {},
45
+ * },
46
+ * }
47
+ * ```
48
+ *
49
+ * @param filePath The entrypoint file path of a language
50
+ *
51
+ * @returns A tuple of the nested object of phrases and the file structure
52
+ *
53
+ */
54
+ export declare const praseLocaleFiles: (filePath: string) => ParsedTuple;
55
+ export declare const syncPhraseKeysAndFileStructure: (baseline: ParsedTuple, targetLocale: string, targetDirectory: string) => Promise<void>;
56
+ export {};
@@ -0,0 +1,249 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { tryThat } from '@silverhand/essentials';
5
+ import ts from 'typescript';
6
+ import { consoleLog } from '../../../utils.js';
7
+ /**
8
+ * Given a entrypoint file path of a language, parse the nested object of
9
+ * phrases and the file structure.
10
+ *
11
+ * @example
12
+ * Given the following file:
13
+ *
14
+ * ```ts
15
+ * import errors from './errors/index.js';
16
+ *
17
+ * const translation = {
18
+ * page_title: 'Anwendungen',
19
+ * errors,
20
+ * };
21
+ * ```
22
+ *
23
+ * The returned object will be:
24
+ *
25
+ * ```ts
26
+ * {
27
+ * page_title: 'Anwendungen',
28
+ * errors: {
29
+ * page_not_found: 'Seite nicht gefunden',
30
+ * },
31
+ * }
32
+ * ```
33
+ *
34
+ * And the file structure will be:
35
+ *
36
+ * ```ts
37
+ * {
38
+ * errors: {
39
+ * filePath: './errors/index.js',
40
+ * structure: {},
41
+ * },
42
+ * }
43
+ * ```
44
+ *
45
+ * @param filePath The entrypoint file path of a language
46
+ *
47
+ * @returns A tuple of the nested object of phrases and the file structure
48
+ *
49
+ */
50
+ export const praseLocaleFiles = (filePath) => {
51
+ const content = readFileSync(filePath, 'utf8');
52
+ const ast = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
53
+ const importIdentifierPath = new Map();
54
+ const traverseNode = (node, nestedObject, fileStructure) => {
55
+ /**
56
+ * Handle property assignment in object literal expression:
57
+ *
58
+ * - Shorthand property assignments (e.g. `{ errors }`) are treated as import
59
+ * - Property assignments will be categorized per its initializer:
60
+ * - Object literal expressions (e.g. `{ errors: { page_not_found: 'Page not found' } }`)
61
+ * will be treated as nested object
62
+ * - String literals (e.g. `{ page_title: 'Applications' }`) or no substitution template
63
+ * literals (e.g. `{ page_title: `Applications` }`) will be treated as string
64
+ * - Others are not supported, and will exit with error
65
+ */
66
+ const handleProperty = (property) => {
67
+ // Treat shorthand property assignment as import
68
+ if (ts.isShorthandPropertyAssignment(property)) {
69
+ const key = property.name.getText();
70
+ const importPath = importIdentifierPath.get(key);
71
+ if (!importPath) {
72
+ consoleLog.fatal(`Cannot find import path for ${key} in ${filePath}`);
73
+ }
74
+ const resolvedPath = path.resolve(path.dirname(filePath), importPath);
75
+ // Recursively parse the nested object from the imported file
76
+ const [phrases, structure] = praseLocaleFiles(resolvedPath);
77
+ // eslint-disable-next-line @silverhand/fp/no-mutation
78
+ nestedObject[key] = phrases;
79
+ // eslint-disable-next-line @silverhand/fp/no-mutation
80
+ fileStructure[key] = {
81
+ filePath: importPath,
82
+ structure,
83
+ };
84
+ }
85
+ if (ts.isPropertyAssignment(property)) {
86
+ const key = property.name.getText();
87
+ // Nested object, recursively parse it
88
+ if (ts.isObjectLiteralExpression(property.initializer)) {
89
+ const [phrases, structure] = traverseNode(property.initializer, {}, {});
90
+ // eslint-disable-next-line @silverhand/fp/no-mutation
91
+ nestedObject[key] = phrases;
92
+ // eslint-disable-next-line @silverhand/fp/no-mutation
93
+ fileStructure[key] = { structure };
94
+ }
95
+ else if (ts.isStringLiteral(property.initializer) ||
96
+ ts.isNoSubstitutionTemplateLiteral(property.initializer)) {
97
+ const value = property.initializer.getText();
98
+ // eslint-disable-next-line @silverhand/fp/no-mutation
99
+ nestedObject[key] = value;
100
+ }
101
+ else {
102
+ consoleLog.fatal('Unsupported property:', property);
103
+ }
104
+ }
105
+ };
106
+ if (ts.isImportDeclaration(node)) {
107
+ const importPath = node.moduleSpecifier.getText().slice(1, -1).replace('.js', '.ts');
108
+ const importIdentifier = node.importClause?.getText();
109
+ // Assuming only default import is used
110
+ if (importIdentifier) {
111
+ importIdentifierPath.set(importIdentifier, importPath);
112
+ }
113
+ }
114
+ else if (ts.isObjectLiteralExpression(node)) {
115
+ for (const property of node.properties) {
116
+ handleProperty(property);
117
+ }
118
+ }
119
+ else {
120
+ node.forEachChild((child) => {
121
+ traverseNode(child, nestedObject, fileStructure);
122
+ });
123
+ }
124
+ return [nestedObject, fileStructure];
125
+ };
126
+ return traverseNode(ast, {}, {});
127
+ };
128
+ const getIdentifier = (filePath) => {
129
+ const filename = path.basename(filePath, '.ts');
130
+ return (filename === 'index' ? path.basename(path.dirname(filePath)) : filename).replaceAll('-', '_');
131
+ };
132
+ /** Traverse the file structure and return an array of imports in the current file. */
133
+ const getCurrentFileImports = (fileStructure) => {
134
+ const imports = new Set();
135
+ for (const [key, value] of Object.entries(fileStructure)) {
136
+ // If the key has a file path, treat it as an import and stop traversing
137
+ // since it's pointing to another file
138
+ if (value.filePath) {
139
+ imports.add([key, value.filePath]);
140
+ }
141
+ // Otherwise, recursively traverse the nested object
142
+ else {
143
+ for (const importEntry of getCurrentFileImports(value.structure)) {
144
+ imports.add(importEntry);
145
+ }
146
+ }
147
+ }
148
+ return [...imports];
149
+ };
150
+ /**
151
+ * Recursively traverse the nested object of phrases and the file structure of
152
+ * the baseline language, and generate the target language directory with the
153
+ * same file structure.
154
+ *
155
+ * Values of the nested object will be replaced with the values of the target
156
+ * language if the key exists; otherwise, the value of the baseline language
157
+ * will be used.
158
+ *
159
+ * @param baseline The baseline language tuple
160
+ * @param targetObject The target language nested object
161
+ * @param targetFilePath The target language entrypoint file path
162
+ * @param isRoot Whether the target file is the root entrypoint
163
+ */
164
+ /* eslint-disable no-await-in-loop */
165
+ const traverseNode = async (baseline, targetObject, targetFilePath, isRoot = false) => {
166
+ const [, baselineStructure] = baseline;
167
+ const targetDirectory = path.dirname(targetFilePath);
168
+ await fs.mkdir(targetDirectory, { recursive: true });
169
+ await fs.writeFile(targetFilePath, '', { flag: 'w+' });
170
+ if (isRoot) {
171
+ await fs.appendFile(targetFilePath, "import type { LocalePhrase } from '../../types.js';\n\n");
172
+ }
173
+ // Write imports first
174
+ const baselineEntries = getCurrentFileImports(baselineStructure);
175
+ for (const [key, value] of baselineEntries
176
+ .slice()
177
+ .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))) {
178
+ const importPath = path.join(targetDirectory, value);
179
+ await fs.appendFile(targetFilePath, `import ${key} from './${path
180
+ .relative(targetDirectory, importPath)
181
+ .replace('.ts', '.js')}';\n`);
182
+ }
183
+ // Add a newline between imports and the object
184
+ if (baselineEntries.length > 0) {
185
+ await fs.appendFile(targetFilePath, '\n');
186
+ }
187
+ // Write the object
188
+ const identifier = getIdentifier(targetFilePath);
189
+ await fs.appendFile(targetFilePath, `const ${identifier} = {\n`);
190
+ // Recursively traverse the nested object of phrases and the file structure
191
+ // of the baseline language
192
+ const traverseObject = async (baseline, targetObject, tabSize) => {
193
+ const [baselineObject, baselineStructure] = baseline;
194
+ for (const [key, value] of Object.entries(baselineObject)) {
195
+ const existingValue = targetObject[key];
196
+ if (typeof value === 'string') {
197
+ // If the key exists in the target language and the value is a string, use
198
+ // the value of the target language; otherwise, use the value of the
199
+ // baseline language and add a comment to indicate that the phrase is
200
+ // untranslated to help identify missing translations.
201
+ await (typeof existingValue === 'string'
202
+ ? fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${existingValue},\n`)
203
+ : fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${value}, // UNTRANSLATED\n`));
204
+ }
205
+ // Not a string, treat it as a nested object or an import
206
+ else {
207
+ const keyStructure = baselineStructure[key];
208
+ // If the key has a file structure, treat it as an import
209
+ if (keyStructure?.filePath) {
210
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key},\n`);
211
+ await traverseNode([value, keyStructure.structure], typeof existingValue === 'object' ? existingValue : {}, path.join(targetDirectory, keyStructure.filePath));
212
+ }
213
+ // Otherwise, treat it as a nested object.
214
+ else {
215
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: {\n`);
216
+ await traverseObject([value, keyStructure?.structure ?? {}], typeof existingValue === 'object' ? existingValue : {}, tabSize + 2);
217
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}},\n`);
218
+ }
219
+ }
220
+ }
221
+ };
222
+ await traverseObject(baseline, targetObject, 2);
223
+ await (isRoot
224
+ ? fs.appendFile(targetFilePath, '} satisfies LocalePhrase;\n\n')
225
+ : fs.appendFile(targetFilePath, '};\n\n'));
226
+ await fs.appendFile(targetFilePath, `export default Object.freeze(${identifier});\n`);
227
+ };
228
+ /* eslint-enable no-await-in-loop */
229
+ export const syncPhraseKeysAndFileStructure = async (baseline, targetLocale, targetDirectory) => {
230
+ const targetEntrypoint = path.join(targetDirectory, 'index.ts');
231
+ const isTargetLocaleExist = existsSync(targetEntrypoint);
232
+ const targetObject = isTargetLocaleExist ? praseLocaleFiles(targetEntrypoint)[0] : {};
233
+ const backupDirectory = targetDirectory + '.bak';
234
+ if (isTargetLocaleExist) {
235
+ await fs.rename(targetDirectory, backupDirectory);
236
+ }
237
+ else {
238
+ consoleLog.warn(`Cannot find ${targetLocale} entrypoint, creating one`);
239
+ }
240
+ await tryThat(traverseNode(baseline, targetObject, path.join(targetDirectory, 'index.ts'), true), (error) => {
241
+ consoleLog.plain();
242
+ consoleLog.error(error);
243
+ consoleLog.plain();
244
+ consoleLog.fatal(`Failed to sync keys for ${targetLocale}, the backup is at ${backupDirectory} for recovery`);
245
+ });
246
+ if (isTargetLocaleExist) {
247
+ await fs.rm(backupDirectory, { recursive: true });
248
+ }
249
+ };
@@ -66,6 +66,7 @@ export declare const packageJson: {
66
66
  "slonik-interceptor-preset": string;
67
67
  "slonik-sql-tag-raw": string;
68
68
  tar: string;
69
+ typescript: string;
69
70
  yargs: string;
70
71
  zod: string;
71
72
  };
@@ -84,7 +85,6 @@ export declare const packageJson: {
84
85
  "lint-staged": string;
85
86
  prettier: string;
86
87
  sinon: string;
87
- typescript: string;
88
88
  };
89
89
  eslintConfig: {
90
90
  extends: string;
@@ -1,6 +1,6 @@
1
1
  export const packageJson = {
2
2
  "name": "@logto/cli",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Logto CLI.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -44,17 +44,17 @@ export const packageJson = {
44
44
  },
45
45
  "dependencies": {
46
46
  "@logto/connector-kit": "workspace:^1.1.1",
47
- "@logto/core-kit": "workspace:^2.0.0",
47
+ "@logto/core-kit": "workspace:^2.0.1",
48
48
  "@logto/language-kit": "workspace:^1.0.0",
49
- "@logto/phrases": "workspace:^1.4.0",
49
+ "@logto/phrases": "workspace:^1.4.1",
50
50
  "@logto/phrases-ui": "workspace:^1.2.0",
51
- "@logto/schemas": "workspace:1.5.0",
51
+ "@logto/schemas": "workspace:1.7.0",
52
52
  "@logto/shared": "workspace:^2.0.0",
53
53
  "@silverhand/essentials": "^2.5.0",
54
54
  "chalk": "^5.0.0",
55
55
  "decamelize": "^6.0.0",
56
56
  "dotenv": "^16.0.0",
57
- "got": "^12.5.3",
57
+ "got": "^13.0.0",
58
58
  "hpagent": "^1.2.0",
59
59
  "inquirer": "^9.0.0",
60
60
  "nanoid": "^4.0.0",
@@ -69,12 +69,13 @@ export const packageJson = {
69
69
  "slonik-interceptor-preset": "^1.2.10",
70
70
  "slonik-sql-tag-raw": "^1.1.4",
71
71
  "tar": "^6.1.11",
72
+ "typescript": "^5.0.0",
72
73
  "yargs": "^17.6.0",
73
74
  "zod": "^3.20.2"
74
75
  },
75
76
  "devDependencies": {
76
- "@silverhand/eslint-config": "3.0.1",
77
- "@silverhand/ts-config": "3.0.0",
77
+ "@silverhand/eslint-config": "4.0.1",
78
+ "@silverhand/ts-config": "4.0.0",
78
79
  "@types/inquirer": "^9.0.0",
79
80
  "@types/jest": "^29.4.0",
80
81
  "@types/node": "^18.11.18",
@@ -82,12 +83,11 @@ export const packageJson = {
82
83
  "@types/sinon": "^10.0.13",
83
84
  "@types/tar": "^6.1.2",
84
85
  "@types/yargs": "^17.0.13",
85
- "eslint": "^8.34.0",
86
+ "eslint": "^8.44.0",
86
87
  "jest": "^29.5.0",
87
88
  "lint-staged": "^13.0.0",
88
- "prettier": "^2.8.2",
89
- "sinon": "^15.0.0",
90
- "typescript": "^5.0.0"
89
+ "prettier": "^3.0.0",
90
+ "sinon": "^15.0.0"
91
91
  },
92
92
  "eslintConfig": {
93
93
  "extends": "@silverhand",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/cli",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Logto CLI.",
5
5
  "author": "Silverhand Inc. <contact@silverhand.io>",
6
6
  "homepage": "https://github.com/logto-io/logto#readme",
@@ -29,17 +29,17 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@logto/connector-kit": "^1.1.1",
32
- "@logto/core-kit": "^2.0.0",
32
+ "@logto/core-kit": "^2.0.1",
33
33
  "@logto/language-kit": "^1.0.0",
34
- "@logto/phrases": "^1.4.0",
34
+ "@logto/phrases": "^1.4.1",
35
35
  "@logto/phrases-ui": "^1.2.0",
36
- "@logto/schemas": "1.5.0",
36
+ "@logto/schemas": "1.7.0",
37
37
  "@logto/shared": "^2.0.0",
38
38
  "@silverhand/essentials": "^2.5.0",
39
39
  "chalk": "^5.0.0",
40
40
  "decamelize": "^6.0.0",
41
41
  "dotenv": "^16.0.0",
42
- "got": "^12.5.3",
42
+ "got": "^13.0.0",
43
43
  "hpagent": "^1.2.0",
44
44
  "inquirer": "^9.0.0",
45
45
  "nanoid": "^4.0.0",
@@ -54,12 +54,13 @@
54
54
  "slonik-interceptor-preset": "^1.2.10",
55
55
  "slonik-sql-tag-raw": "^1.1.4",
56
56
  "tar": "^6.1.11",
57
+ "typescript": "^5.0.0",
57
58
  "yargs": "^17.6.0",
58
59
  "zod": "^3.20.2"
59
60
  },
60
61
  "devDependencies": {
61
- "@silverhand/eslint-config": "3.0.1",
62
- "@silverhand/ts-config": "3.0.0",
62
+ "@silverhand/eslint-config": "4.0.1",
63
+ "@silverhand/ts-config": "4.0.0",
63
64
  "@types/inquirer": "^9.0.0",
64
65
  "@types/jest": "^29.4.0",
65
66
  "@types/node": "^18.11.18",
@@ -67,12 +68,11 @@
67
68
  "@types/sinon": "^10.0.13",
68
69
  "@types/tar": "^6.1.2",
69
70
  "@types/yargs": "^17.0.13",
70
- "eslint": "^8.34.0",
71
+ "eslint": "^8.44.0",
71
72
  "jest": "^29.5.0",
72
73
  "lint-staged": "^13.0.0",
73
- "prettier": "^2.8.2",
74
- "sinon": "^15.0.0",
75
- "typescript": "^5.0.0"
74
+ "prettier": "^3.0.0",
75
+ "sinon": "^15.0.0"
76
76
  },
77
77
  "eslintConfig": {
78
78
  "extends": "@silverhand",