@logto/cli 1.7.0 → 1.9.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 (37) hide show
  1. package/lib/commands/connector/add.js +2 -2
  2. package/lib/commands/connector/link.js +2 -2
  3. package/lib/commands/connector/utils.d.ts +2 -3
  4. package/lib/commands/connector/utils.js +3 -50
  5. package/lib/commands/database/alteration/index.js +45 -29
  6. package/lib/commands/database/alteration/utils.d.ts +1 -0
  7. package/lib/commands/database/alteration/utils.js +8 -0
  8. package/lib/commands/database/config.js +17 -3
  9. package/lib/commands/database/seed/cloud.d.ts +8 -0
  10. package/lib/commands/database/seed/cloud.js +24 -1
  11. package/lib/commands/database/seed/index.js +0 -2
  12. package/lib/commands/database/seed/oidc-config.js +2 -2
  13. package/lib/commands/database/seed/tables.js +11 -10
  14. package/lib/commands/database/utils.d.ts +5 -1
  15. package/lib/commands/database/utils.js +8 -3
  16. package/lib/commands/install/index.js +7 -7
  17. package/lib/commands/install/utils.d.ts +3 -4
  18. package/lib/commands/install/utils.js +6 -8
  19. package/lib/commands/translate/create.js +4 -5
  20. package/lib/commands/translate/list-tags.js +2 -2
  21. package/lib/commands/translate/prompts.d.ts +2 -1
  22. package/lib/commands/translate/prompts.js +10 -7
  23. package/lib/commands/translate/sync-keys/index.js +7 -15
  24. package/lib/commands/translate/sync-keys/utils.d.ts +6 -5
  25. package/lib/commands/translate/sync-keys/utils.js +46 -22
  26. package/lib/commands/translate/sync.js +8 -5
  27. package/lib/commands/translate/utils.d.ts +1 -1
  28. package/lib/commands/translate/utils.js +1 -2
  29. package/lib/connector/utils.d.ts +3 -3
  30. package/lib/connector/utils.js +19 -6
  31. package/lib/constants.d.ts +2 -0
  32. package/lib/constants.js +4 -0
  33. package/lib/package-json.d.ts +1 -1
  34. package/lib/package-json.js +9 -9
  35. package/lib/utils.d.ts +2 -0
  36. package/lib/utils.js +66 -4
  37. package/package.json +9 -9
@@ -1,34 +1,37 @@
1
1
  import { languages } from '@logto/language-kit';
2
2
  import { conditionalString } from '@silverhand/essentials';
3
+ export const untranslatedMark = '/** UNTRANSLATED */';
3
4
  /**
4
5
  * Note:
5
- * The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1200.
6
+ * The input token limit of GPT 3.5 is 2048, the following prompt tokens with sourceFileContent is about 1600.
6
7
  * Remember to check the token limit before adding more prompt.
7
8
  * Tokens can be counted in https://platform.openai.com/tokenizer
8
9
  */
9
10
  export const getTranslationPromptMessages = ({ sourceFileContent, targetLanguage, extraPrompt, }) => [
10
11
  {
11
12
  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)}
13
+ content: `You are a assistant translator and will receive a TypeScript object. Traverse and find object values with "${untranslatedMark}" annotation on the top, then translate these values to target locale "${languages[targetLanguage]}". Remove the "${untranslatedMark}" annotations from output. Escape the single quotes (if any) in translated results by prepending a backslash. Keep the interpolation double curly brackets and their inner values intact. Make sure there is a space between the CJK and non-CJK characters. Prefer using "你" instead of "您" in Chinese. Do not include sample code snippet below in the final output. ${conditionalString(extraPrompt)}
13
14
 
14
- Take Chinese as an example, if the input is:
15
+ Take translating to zh-CN as an example, if the input is:
15
16
  \`\`\`ts
16
17
  import others from './others.js';
17
18
 
18
19
  const translation = {
19
- hello: '你好',
20
- world: 'world', // UNTRANSLATED
20
+ hello: 'Hello',
21
+ ${untranslatedMark}
22
+ world: 'world',
21
23
  others,
22
24
  };
23
25
 
24
26
  export default translation;
25
27
  \`\`\`
26
- the output should be:
28
+
29
+ Then the output should be:
27
30
  \`\`\`ts
28
31
  import others from './others.js';
29
32
 
30
33
  const translation = {
31
- hello: '你好',
34
+ hello: 'Hello',
32
35
  world: '世界',
33
36
  others,
34
37
  };
@@ -1,13 +1,9 @@
1
- import { execFile } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
- import { promisify } from 'node:util';
5
3
  import { isLanguageTag } from '@logto/language-kit';
6
4
  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);
5
+ import { consoleLog, inquireInstancePath, lintLocaleFiles } from '../../../utils.js';
6
+ import { parseLocaleFiles, syncPhraseKeysAndFileStructure } from './utils.js';
11
7
  const syncKeys = {
12
8
  command: ['sync-keys', 'sk'],
13
9
  describe: [
@@ -26,7 +22,7 @@ const syncKeys = {
26
22
  .option('package', {
27
23
  alias: 'pkg',
28
24
  type: 'string',
29
- describe: 'The package name of the phrases, one of `phrases` or `phrases-ui`',
25
+ describe: 'The package name of the phrases, one of `phrases` or `phrases-experience`',
30
26
  default: 'phrases',
31
27
  })
32
28
  .option('target', {
@@ -50,14 +46,14 @@ const syncKeys = {
50
46
  if (baselineTag === targetTag) {
51
47
  consoleLog.fatal('Baseline and target cannot be the same');
52
48
  }
53
- if (packageName !== 'phrases' && packageName !== 'phrases-ui') {
54
- consoleLog.fatal('Invalid package name, expected `phrases` or `phrases-ui`');
49
+ if (packageName !== 'phrases' && packageName !== 'phrases-experience') {
50
+ consoleLog.fatal('Invalid package name, expected `phrases` or `phrases-experience`');
55
51
  }
56
52
  const instancePath = await inquireInstancePath(inputPath);
57
53
  const phrasesPath = path.join(instancePath, 'packages', packageName);
58
54
  const localesPath = path.join(phrasesPath, 'src/locales');
59
55
  const entrypoint = path.join(localesPath, baselineTag.toLowerCase(), 'index.ts');
60
- const baseline = praseLocaleFiles(entrypoint);
56
+ const baseline = parseLocaleFiles(entrypoint);
61
57
  const targetLocales = targetTag === 'all' ? fs.readdirSync(localesPath) : [targetTag.toLowerCase()];
62
58
  /* eslint-disable no-await-in-loop */
63
59
  for (const target of targetLocales) {
@@ -73,11 +69,7 @@ const syncKeys = {
73
69
  }
74
70
  /* eslint-enable no-await-in-loop */
75
71
  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');
72
+ void lintLocaleFiles(instancePath, packageName);
81
73
  }
82
74
  },
83
75
  };
@@ -5,7 +5,7 @@ type FileStructure = {
5
5
  };
6
6
  };
7
7
  type NestedPhraseObject = {
8
- [key: string]: string | NestedPhraseObject;
8
+ [key: string]: [phrase: string, isTranslated: boolean] | NestedPhraseObject;
9
9
  };
10
10
  type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
11
11
  /**
@@ -19,7 +19,8 @@ type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
19
19
  * import errors from './errors/index.js';
20
20
  *
21
21
  * const translation = {
22
- * page_title: 'Anwendungen',
22
+ * // UNTRANSLATED
23
+ * page_title: 'Applications',
23
24
  * errors,
24
25
  * };
25
26
  * ```
@@ -28,9 +29,9 @@ type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
28
29
  *
29
30
  * ```ts
30
31
  * {
31
- * page_title: 'Anwendungen',
32
+ * page_title: ['Anwendungen', false],
32
33
  * errors: {
33
- * page_not_found: 'Seite nicht gefunden',
34
+ * page_not_found: ['Seite nicht gefunden', true],
34
35
  * },
35
36
  * }
36
37
  * ```
@@ -51,6 +52,6 @@ type ParsedTuple = readonly [NestedPhraseObject, FileStructure];
51
52
  * @returns A tuple of the nested object of phrases and the file structure
52
53
  *
53
54
  */
54
- export declare const praseLocaleFiles: (filePath: string) => ParsedTuple;
55
+ export declare const parseLocaleFiles: (filePath: string) => ParsedTuple;
55
56
  export declare const syncPhraseKeysAndFileStructure: (baseline: ParsedTuple, targetLocale: string, targetDirectory: string) => Promise<void>;
56
57
  export {};
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { tryThat } from '@silverhand/essentials';
5
5
  import ts from 'typescript';
6
6
  import { consoleLog } from '../../../utils.js';
7
+ const untranslatedMark = 'UNTRANSLATED';
7
8
  /**
8
9
  * Given a entrypoint file path of a language, parse the nested object of
9
10
  * phrases and the file structure.
@@ -15,7 +16,8 @@ import { consoleLog } from '../../../utils.js';
15
16
  * import errors from './errors/index.js';
16
17
  *
17
18
  * const translation = {
18
- * page_title: 'Anwendungen',
19
+ * // UNTRANSLATED
20
+ * page_title: 'Applications',
19
21
  * errors,
20
22
  * };
21
23
  * ```
@@ -24,9 +26,9 @@ import { consoleLog } from '../../../utils.js';
24
26
  *
25
27
  * ```ts
26
28
  * {
27
- * page_title: 'Anwendungen',
29
+ * page_title: ['Anwendungen', false],
28
30
  * errors: {
29
- * page_not_found: 'Seite nicht gefunden',
31
+ * page_not_found: ['Seite nicht gefunden', true],
30
32
  * },
31
33
  * }
32
34
  * ```
@@ -47,7 +49,7 @@ import { consoleLog } from '../../../utils.js';
47
49
  * @returns A tuple of the nested object of phrases and the file structure
48
50
  *
49
51
  */
50
- export const praseLocaleFiles = (filePath) => {
52
+ export const parseLocaleFiles = (filePath) => {
51
53
  const content = readFileSync(filePath, 'utf8');
52
54
  const ast = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
53
55
  const importIdentifierPath = new Map();
@@ -73,30 +75,47 @@ export const praseLocaleFiles = (filePath) => {
73
75
  }
74
76
  const resolvedPath = path.resolve(path.dirname(filePath), importPath);
75
77
  // 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
+ const [phrases, structure] = parseLocaleFiles(resolvedPath);
79
+ /* eslint-disable @silverhand/fp/no-mutation */
78
80
  nestedObject[key] = phrases;
79
- // eslint-disable-next-line @silverhand/fp/no-mutation
80
81
  fileStructure[key] = {
81
82
  filePath: importPath,
82
83
  structure,
83
84
  };
85
+ /* eslint-enable @silverhand/fp/no-mutation */
84
86
  }
85
87
  if (ts.isPropertyAssignment(property)) {
86
88
  const key = property.name.getText();
87
89
  // Nested object, recursively parse it
88
90
  if (ts.isObjectLiteralExpression(property.initializer)) {
89
91
  const [phrases, structure] = traverseNode(property.initializer, {}, {});
90
- // eslint-disable-next-line @silverhand/fp/no-mutation
92
+ /* eslint-disable @silverhand/fp/no-mutation */
91
93
  nestedObject[key] = phrases;
92
- // eslint-disable-next-line @silverhand/fp/no-mutation
93
94
  fileStructure[key] = { structure };
95
+ /* eslint-enable @silverhand/fp/no-mutation */
94
96
  }
95
97
  else if (ts.isStringLiteral(property.initializer) ||
96
98
  ts.isNoSubstitutionTemplateLiteral(property.initializer)) {
97
99
  const value = property.initializer.getText();
98
- // eslint-disable-next-line @silverhand/fp/no-mutation
99
- nestedObject[key] = value;
100
+ const commentRanges = ts.getLeadingCommentRanges(property.getFullText(), 0);
101
+ if (commentRanges?.[0]) {
102
+ if (commentRanges.length > 1) {
103
+ consoleLog.fatal('Multiple comments found for property:', property);
104
+ }
105
+ const commentRange = commentRanges[0];
106
+ const comment = property.getFullText().slice(commentRange.pos, commentRange.end);
107
+ if (comment.includes(untranslatedMark)) {
108
+ // eslint-disable-next-line @silverhand/fp/no-mutation
109
+ nestedObject[key] = [value, false];
110
+ }
111
+ else {
112
+ consoleLog.fatal('Unsupported comment:', comment);
113
+ }
114
+ }
115
+ else {
116
+ // eslint-disable-next-line @silverhand/fp/no-mutation
117
+ nestedObject[key] = [value, true];
118
+ }
100
119
  }
101
120
  else {
102
121
  consoleLog.fatal('Unsupported property:', property);
@@ -193,14 +212,19 @@ const traverseNode = async (baseline, targetObject, targetFilePath, isRoot = fal
193
212
  const [baselineObject, baselineStructure] = baseline;
194
213
  for (const [key, value] of Object.entries(baselineObject)) {
195
214
  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`));
215
+ if (Array.isArray(value)) {
216
+ const [phrase] = value;
217
+ // If the key exists in the target language and the value is a translated
218
+ // string, use the value of the target language; otherwise, use the value
219
+ // of the baseline language and add a comment to indicate that the phrase
220
+ // is untranslated to help identify missing translations.
221
+ if (Array.isArray(existingValue) && existingValue[1]) {
222
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${existingValue[0]},\n`);
223
+ }
224
+ else {
225
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}/** ${untranslatedMark} */\n`);
226
+ await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: ${phrase},\n`);
227
+ }
204
228
  }
205
229
  // Not a string, treat it as a nested object or an import
206
230
  else {
@@ -208,12 +232,12 @@ const traverseNode = async (baseline, targetObject, targetFilePath, isRoot = fal
208
232
  // If the key has a file structure, treat it as an import
209
233
  if (keyStructure?.filePath) {
210
234
  await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key},\n`);
211
- await traverseNode([value, keyStructure.structure], typeof existingValue === 'object' ? existingValue : {}, path.join(targetDirectory, keyStructure.filePath));
235
+ await traverseNode([value, keyStructure.structure], Array.isArray(existingValue) ? {} : existingValue ?? {}, path.join(targetDirectory, keyStructure.filePath));
212
236
  }
213
237
  // Otherwise, treat it as a nested object.
214
238
  else {
215
239
  await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}${key}: {\n`);
216
- await traverseObject([value, keyStructure?.structure ?? {}], typeof existingValue === 'object' ? existingValue : {}, tabSize + 2);
240
+ await traverseObject([value, keyStructure?.structure ?? {}], Array.isArray(existingValue) ? {} : existingValue ?? {}, tabSize + 2);
217
241
  await fs.appendFile(targetFilePath, `${' '.repeat(tabSize)}},\n`);
218
242
  }
219
243
  }
@@ -229,7 +253,7 @@ const traverseNode = async (baseline, targetObject, targetFilePath, isRoot = fal
229
253
  export const syncPhraseKeysAndFileStructure = async (baseline, targetLocale, targetDirectory) => {
230
254
  const targetEntrypoint = path.join(targetDirectory, 'index.ts');
231
255
  const isTargetLocaleExist = existsSync(targetEntrypoint);
232
- const targetObject = isTargetLocaleExist ? praseLocaleFiles(targetEntrypoint)[0] : {};
256
+ const targetObject = isTargetLocaleExist ? parseLocaleFiles(targetEntrypoint)[0] : {};
233
257
  const backupDirectory = targetDirectory + '.bak';
234
258
  if (isTargetLocaleExist) {
235
259
  await fs.rename(targetDirectory, backupDirectory);
@@ -1,8 +1,8 @@
1
1
  import { languages } from '@logto/language-kit';
2
2
  import { isBuiltInLanguageTag as isPhrasesBuiltInLanguageTag } from '@logto/phrases';
3
- import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-ui';
3
+ import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
4
4
  import PQueue from 'p-queue';
5
- import { inquireInstancePath } from '../connector/utils.js';
5
+ import { inquireInstancePath, lintLocaleFiles } from '../../utils.js';
6
6
  import { baseLanguage, syncTranslation } from './utils.js';
7
7
  const sync = {
8
8
  command: ['sync'],
@@ -19,22 +19,25 @@ const sync = {
19
19
  verbose: false,
20
20
  queue,
21
21
  };
22
+ /* eslint-disable no-await-in-loop */
22
23
  if (isPhrasesBuiltInLanguageTag(languageTag)) {
23
- void syncTranslation({
24
+ await syncTranslation({
24
25
  ...baseOptions,
25
26
  packageName: 'phrases',
26
27
  languageTag,
27
28
  });
28
29
  }
29
30
  if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
30
- void syncTranslation({
31
+ await syncTranslation({
31
32
  ...baseOptions,
32
- packageName: 'phrases-ui',
33
+ packageName: 'phrases-experience',
33
34
  languageTag,
34
35
  });
35
36
  }
37
+ /* eslint-enable no-await-in-loop */
36
38
  }
37
39
  await queue.onIdle();
40
+ void lintLocaleFiles(instancePath);
38
41
  },
39
42
  };
40
43
  export default sync;
@@ -5,7 +5,7 @@ export declare const readLocaleFiles: (directory: string) => Promise<string[]>;
5
5
  export declare const readBaseLocaleFiles: (directory: string) => Promise<string[]>;
6
6
  export type TranslationOptions = {
7
7
  instancePath: string;
8
- packageName: 'phrases' | 'phrases-ui';
8
+ packageName: 'phrases' | 'phrases-experience';
9
9
  languageTag: LanguageTag;
10
10
  verbose?: boolean;
11
11
  queue?: PQueue;
@@ -5,8 +5,8 @@ import { conditionalString } from '@silverhand/essentials';
5
5
  import PQueue from 'p-queue';
6
6
  import { consoleLog } from '../../utils.js';
7
7
  import { createOpenaiApi, translate } from './openai.js';
8
+ import { untranslatedMark } from './prompts.js';
8
9
  export const baseLanguage = 'en';
9
- const untranslatedMark = '// UNTRANSLATED';
10
10
  export const readLocaleFiles = async (directory) => {
11
11
  const entities = await fs.readdir(directory, { withFileTypes: true });
12
12
  const result = await Promise.all(entities.map(async (entity) => {
@@ -91,7 +91,6 @@ export const syncTranslation = async ({ instancePath, packageName, languageTag,
91
91
  api: openai,
92
92
  sourceFilePath: targetLocaleFile,
93
93
  targetLanguage: languageTag,
94
- extraPrompt: `Object values without an "${untranslatedMark}" mark should be skipped and keep its original value. Remember to remove the "${untranslatedMark}" mark with the spaces before and after it in the output content.`,
95
94
  });
96
95
  if (!result) {
97
96
  consoleLog.fatal(`Unable to translate ${targetLocaleFile}`);
@@ -1,10 +1,10 @@
1
- import type { AllConnector, BaseConnector, ConnectorMetadata, GetConnectorConfig } from '@logto/connector-kit';
1
+ import type { AllConnector, BaseConnector, ConnectorMetadata, GetConnectorConfig, GetCloudServiceClient } from '@logto/connector-kit';
2
2
  import { ConnectorType } from '@logto/connector-kit';
3
3
  import type { ConnectorFactory } from './types.js';
4
4
  export declare function validateConnectorModule(connector: Partial<BaseConnector<ConnectorType>>): asserts connector is BaseConnector<ConnectorType>;
5
- export declare const readUrl: (url: string, baseUrl: string, type: 'text' | 'svg') => Promise<string>;
5
+ export declare const readUrl: (url: string, baseUrl: string, type: 'text' | 'image') => Promise<string>;
6
6
  export declare const parseMetadata: (metadata: AllConnector['metadata'], packagePath: string) => Promise<AllConnector['metadata']>;
7
- export declare const buildRawConnector: <T extends AllConnector = AllConnector>(connectorFactory: ConnectorFactory<T>, getConnectorConfig?: GetConnectorConfig) => Promise<{
7
+ export declare const buildRawConnector: <T extends AllConnector = AllConnector>(connectorFactory: ConnectorFactory<T>, getConnectorConfig?: GetConnectorConfig, getCloudServiceClient?: GetCloudServiceClient) => Promise<{
8
8
  rawConnector: T;
9
9
  rawMetadata: ConnectorMetadata;
10
10
  }>;
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
5
+ import { consoleLog } from '../utils.js';
5
6
  import { notImplemented } from './consts.js';
6
7
  export function validateConnectorModule(connector) {
7
8
  if (!connector.metadata) {
@@ -14,6 +15,11 @@ export function validateConnectorModule(connector) {
14
15
  throw new ConnectorError(ConnectorErrorCodes.UnexpectedType);
15
16
  }
16
17
  }
18
+ const supportedImageTypes = Object.freeze({
19
+ '.svg': 'image/svg+xml',
20
+ '.png': 'image/png',
21
+ });
22
+ const isSupportedImageType = (extension) => Object.keys(supportedImageTypes).includes(extension);
17
23
  export const readUrl = async (url, baseUrl, type) => {
18
24
  if (!url) {
19
25
  return url;
@@ -24,25 +30,32 @@ export const readUrl = async (url, baseUrl, type) => {
24
30
  if (!existsSync(path.join(baseUrl, url))) {
25
31
  return url;
26
32
  }
27
- if (type === 'svg') {
28
- const data = await readFile(path.join(baseUrl, url));
29
- return `data:image/svg+xml;base64,${data.toString('base64')}`;
33
+ if (type === 'image') {
34
+ const filePath = path.join(baseUrl, url);
35
+ const extension = path.extname(filePath);
36
+ if (!isSupportedImageType(extension)) {
37
+ consoleLog.warn(`[readUrl] unexpected image type: ${filePath}, only support ".svg" and ".png". Falling back to empty string.`);
38
+ return '';
39
+ }
40
+ const data = await readFile(filePath);
41
+ return `data:${supportedImageTypes[extension]};base64,${data.toString('base64')}`;
30
42
  }
31
43
  return readFile(path.join(baseUrl, url), 'utf8');
32
44
  };
33
45
  export const parseMetadata = async (metadata, packagePath) => {
34
46
  return {
35
47
  ...metadata,
36
- logo: await readUrl(metadata.logo, packagePath, 'svg'),
37
- logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
48
+ logo: await readUrl(metadata.logo, packagePath, 'image'),
49
+ logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'image')),
38
50
  readme: await readUrl(metadata.readme, packagePath, 'text'),
39
51
  configTemplate: metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')),
40
52
  };
41
53
  };
42
- export const buildRawConnector = async (connectorFactory, getConnectorConfig) => {
54
+ export const buildRawConnector = async (connectorFactory, getConnectorConfig, getCloudServiceClient) => {
43
55
  const { createConnector, path: packagePath } = connectorFactory;
44
56
  const rawConnector = await createConnector({
45
57
  getConfig: getConnectorConfig ?? notImplemented,
58
+ getCloudServiceClient,
46
59
  });
47
60
  validateConnectorModule(rawConnector);
48
61
  const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
@@ -1 +1,3 @@
1
+ export declare const defaultPath: string;
2
+ export declare const coreDirectory = "packages/core";
1
3
  export declare const connectorDirectory = "connectors";
package/lib/constants.js CHANGED
@@ -1 +1,5 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export const defaultPath = path.join(os.homedir(), 'logto');
4
+ export const coreDirectory = 'packages/core';
1
5
  export const connectorDirectory = 'connectors';
@@ -44,7 +44,7 @@ export declare const packageJson: {
44
44
  "@logto/core-kit": string;
45
45
  "@logto/language-kit": string;
46
46
  "@logto/phrases": string;
47
- "@logto/phrases-ui": string;
47
+ "@logto/phrases-experience": string;
48
48
  "@logto/schemas": string;
49
49
  "@logto/shared": string;
50
50
  "@silverhand/essentials": string;
@@ -1,6 +1,6 @@
1
1
  export const packageJson = {
2
2
  "name": "@logto/cli",
3
- "version": "1.7.0",
3
+ "version": "1.9.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,13 +44,13 @@ export const packageJson = {
44
44
  },
45
45
  "dependencies": {
46
46
  "@logto/connector-kit": "workspace:^1.1.1",
47
- "@logto/core-kit": "workspace:^2.0.1",
47
+ "@logto/core-kit": "workspace:^2.1.0",
48
48
  "@logto/language-kit": "workspace:^1.0.0",
49
- "@logto/phrases": "workspace:^1.4.1",
50
- "@logto/phrases-ui": "workspace:^1.2.0",
51
- "@logto/schemas": "workspace:1.7.0",
52
- "@logto/shared": "workspace:^2.0.0",
53
- "@silverhand/essentials": "^2.5.0",
49
+ "@logto/phrases": "workspace:^1.5.0",
50
+ "@logto/phrases-experience": "workspace:^1.3.0",
51
+ "@logto/schemas": "workspace:1.9.0",
52
+ "@logto/shared": "workspace:^2.0.1",
53
+ "@silverhand/essentials": "^2.8.4",
54
54
  "chalk": "^5.0.0",
55
55
  "decamelize": "^6.0.0",
56
56
  "dotenv": "^16.0.0",
@@ -61,7 +61,7 @@ export const packageJson = {
61
61
  "ora": "^6.1.2",
62
62
  "p-limit": "^4.0.0",
63
63
  "p-queue": "^7.3.4",
64
- "p-retry": "^5.1.2",
64
+ "p-retry": "^6.0.0",
65
65
  "pg-protocol": "^1.6.0",
66
66
  "roarr": "^7.11.0",
67
67
  "semver": "^7.3.8",
@@ -85,7 +85,7 @@ export const packageJson = {
85
85
  "@types/yargs": "^17.0.13",
86
86
  "eslint": "^8.44.0",
87
87
  "jest": "^29.5.0",
88
- "lint-staged": "^13.0.0",
88
+ "lint-staged": "^14.0.0",
89
89
  "prettier": "^3.0.0",
90
90
  "sinon": "^15.0.0"
91
91
  },
package/lib/utils.d.ts CHANGED
@@ -28,8 +28,10 @@ export declare const getCliConfigWithPrompt: ({ key, readableKey, comments, defa
28
28
  * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1.
29
29
  */
30
30
  export declare function findLastIndex<T>(array: readonly T[], predicate: (value: T, index: number, object: readonly T[]) => boolean): number;
31
+ export declare const inquireInstancePath: (initialPath?: string) => Promise<string>;
31
32
  export type ConnectorPackage = {
32
33
  name: string;
33
34
  path: string;
34
35
  };
35
36
  export declare const getConnectorPackagesFromDirectory: (directory: string) => Promise<ConnectorPackage[]>;
37
+ export declare const lintLocaleFiles: (instancePath: string, packageName?: 'phrases' | 'phrases-experience') => Promise<void>;
package/lib/utils.js CHANGED
@@ -1,16 +1,18 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execSync, execFile } from 'node:child_process';
2
2
  import { createWriteStream, existsSync } from 'node:fs';
3
3
  import { readdir, readFile } from 'node:fs/promises';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
+ import { promisify } from 'node:util';
6
7
  import { ConsoleLog } from '@logto/shared';
7
- import { conditionalString } from '@silverhand/essentials';
8
+ import { assert, conditionalString } from '@silverhand/essentials';
8
9
  import chalk from 'chalk';
9
10
  import { got } from 'got';
10
11
  import { HttpsProxyAgent } from 'hpagent';
11
12
  import inquirer from 'inquirer';
12
13
  import ora from 'ora';
13
14
  import { z } from 'zod';
15
+ import { coreDirectory, defaultPath } from './constants.js';
14
16
  export const safeExecSync = (command) => {
15
17
  try {
16
18
  return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
@@ -40,7 +42,7 @@ export const downloadFile = async (url, destination) => {
40
42
  });
41
43
  const spinner = ora({
42
44
  text: 'Connecting',
43
- prefixText: chalk.blue('[info]'),
45
+ prefixText: ConsoleLog.prefixes.info,
44
46
  }).start();
45
47
  stream.pipe(file);
46
48
  return new Promise((resolve, reject) => {
@@ -66,7 +68,7 @@ export const getPathInModule = (moduleName, relativePath = '/') =>
66
68
  // https://stackoverflow.com/a/49455609/12514940
67
69
  path.join(path.dirname(createRequire(import.meta.url).resolve(`${moduleName}/package.json`)), relativePath);
68
70
  export const oraPromise = async (promise, options, exitOnError = false) => {
69
- const spinner = ora(options).start();
71
+ const spinner = ora({ prefixText: ConsoleLog.prefixes.info, ...options }).start();
70
72
  try {
71
73
  const result = await promise;
72
74
  spinner.succeed();
@@ -120,6 +122,49 @@ export function findLastIndex(array, predicate) {
120
122
  }
121
123
  return -1;
122
124
  }
125
+ const buildPathErrorMessage = (value) => `The path ${chalk.green(value)} does not contain a Logto instance. Please try another.`;
126
+ const validatePath = async (value) => {
127
+ const corePackageJsonPath = path.resolve(path.join(value, coreDirectory, 'package.json'));
128
+ if (!existsSync(corePackageJsonPath)) {
129
+ return buildPathErrorMessage(value);
130
+ }
131
+ const packageJson = await readFile(corePackageJsonPath, { encoding: 'utf8' });
132
+ const packageName = await z
133
+ .object({ name: z.string() })
134
+ .parseAsync(JSON.parse(packageJson))
135
+ .then(({ name }) => name)
136
+ .catch(() => '');
137
+ if (packageName !== '@logto/core') {
138
+ return buildPathErrorMessage(value);
139
+ }
140
+ return true;
141
+ };
142
+ export const inquireInstancePath = async (initialPath) => {
143
+ const inquire = async () => {
144
+ if (!initialPath && (await validatePath('.')) === true) {
145
+ return path.resolve('.');
146
+ }
147
+ if (!isTty()) {
148
+ assert(initialPath, new Error('Path is missing'));
149
+ return initialPath;
150
+ }
151
+ const { instancePath } = await inquirer.prompt({
152
+ name: 'instancePath',
153
+ message: 'Where is your Logto instance?',
154
+ type: 'input',
155
+ default: defaultPath,
156
+ filter: (value) => value.trim(),
157
+ validate: validatePath,
158
+ }, { instancePath: initialPath });
159
+ return instancePath;
160
+ };
161
+ const instancePath = await inquire();
162
+ const validated = await validatePath(instancePath);
163
+ if (validated !== true) {
164
+ consoleLog.fatal(validated);
165
+ }
166
+ return instancePath;
167
+ };
123
168
  const getConnectorPackageName = async (directory) => {
124
169
  const filePath = path.join(directory, 'package.json');
125
170
  if (!existsSync(filePath)) {
@@ -139,3 +184,20 @@ export const getConnectorPackagesFromDirectory = async (directory) => {
139
184
  }));
140
185
  return rawPackages.filter((packageInfo) => typeof packageInfo.name === 'string');
141
186
  };
187
+ const execPromise = promisify(execFile);
188
+ export const lintLocaleFiles = async (
189
+ /** Logto instance path */
190
+ instancePath,
191
+ /** Target package name, ignore to lint both packages */
192
+ packageName) => {
193
+ const spinner = ora({
194
+ text: 'Running `eslint --fix` for locales',
195
+ }).start();
196
+ const targetPackages = packageName ? [packageName] : ['phrases', 'phrases-experience'];
197
+ await Promise.all(targetPackages.map(async (packageName) => {
198
+ const phrasesPath = path.join(instancePath, 'packages', packageName);
199
+ const localesPath = path.join(phrasesPath, 'src/locales');
200
+ await execPromise('pnpm', ['eslint', '--ext', '.ts', path.relative(phrasesPath, localesPath), '--fix'], { cwd: phrasesPath });
201
+ }));
202
+ spinner.succeed('Ran `eslint --fix` for locales');
203
+ };