@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.
- package/lib/commands/connector/add.js +2 -2
- package/lib/commands/connector/link.js +2 -2
- package/lib/commands/connector/utils.d.ts +2 -3
- package/lib/commands/connector/utils.js +3 -50
- package/lib/commands/database/alteration/index.js +45 -29
- package/lib/commands/database/alteration/utils.d.ts +1 -0
- package/lib/commands/database/alteration/utils.js +8 -0
- package/lib/commands/database/config.js +17 -3
- package/lib/commands/database/seed/cloud.d.ts +8 -0
- package/lib/commands/database/seed/cloud.js +24 -1
- package/lib/commands/database/seed/index.js +0 -2
- package/lib/commands/database/seed/oidc-config.js +2 -2
- package/lib/commands/database/seed/tables.js +11 -10
- package/lib/commands/database/utils.d.ts +5 -1
- package/lib/commands/database/utils.js +8 -3
- package/lib/commands/install/index.js +7 -7
- package/lib/commands/install/utils.d.ts +3 -4
- package/lib/commands/install/utils.js +6 -8
- package/lib/commands/translate/create.js +4 -5
- package/lib/commands/translate/list-tags.js +2 -2
- package/lib/commands/translate/prompts.d.ts +2 -1
- package/lib/commands/translate/prompts.js +10 -7
- package/lib/commands/translate/sync-keys/index.js +7 -15
- package/lib/commands/translate/sync-keys/utils.d.ts +6 -5
- package/lib/commands/translate/sync-keys/utils.js +46 -22
- package/lib/commands/translate/sync.js +8 -5
- package/lib/commands/translate/utils.d.ts +1 -1
- package/lib/commands/translate/utils.js +1 -2
- package/lib/connector/utils.d.ts +3 -3
- package/lib/connector/utils.js +19 -6
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +4 -0
- package/lib/package-json.d.ts +1 -1
- package/lib/package-json.js +9 -9
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +66 -4
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
+
hello: 'Hello',
|
|
21
|
+
${untranslatedMark}
|
|
22
|
+
world: 'world',
|
|
21
23
|
others,
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export default translation;
|
|
25
27
|
\`\`\`
|
|
26
|
-
|
|
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 {
|
|
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-
|
|
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-
|
|
54
|
-
consoleLog.fatal('Invalid package name, expected `phrases` or `phrases-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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] =
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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 (
|
|
197
|
-
|
|
198
|
-
// the
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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],
|
|
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 ?? {}],
|
|
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 ?
|
|
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-
|
|
3
|
+
import { isBuiltInLanguageTag as isPhrasesUiBuiltInLanguageTag } from '@logto/phrases-experience';
|
|
4
4
|
import PQueue from 'p-queue';
|
|
5
|
-
import { inquireInstancePath } from '
|
|
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
|
-
|
|
24
|
+
await syncTranslation({
|
|
24
25
|
...baseOptions,
|
|
25
26
|
packageName: 'phrases',
|
|
26
27
|
languageTag,
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
if (isPhrasesUiBuiltInLanguageTag(languageTag)) {
|
|
30
|
-
|
|
31
|
+
await syncTranslation({
|
|
31
32
|
...baseOptions,
|
|
32
|
-
packageName: 'phrases-
|
|
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-
|
|
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}`);
|
package/lib/connector/utils.d.ts
CHANGED
|
@@ -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' | '
|
|
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
|
}>;
|
package/lib/connector/utils.js
CHANGED
|
@@ -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 === '
|
|
28
|
-
const
|
|
29
|
-
|
|
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, '
|
|
37
|
-
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, '
|
|
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);
|
package/lib/constants.d.ts
CHANGED
package/lib/constants.js
CHANGED
package/lib/package-json.d.ts
CHANGED
|
@@ -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-
|
|
47
|
+
"@logto/phrases-experience": string;
|
|
48
48
|
"@logto/schemas": string;
|
|
49
49
|
"@logto/shared": string;
|
|
50
50
|
"@silverhand/essentials": string;
|
package/lib/package-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const packageJson = {
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.
|
|
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
|
|
47
|
+
"@logto/core-kit": "workspace:^2.1.0",
|
|
48
48
|
"@logto/language-kit": "workspace:^1.0.0",
|
|
49
|
-
"@logto/phrases": "workspace:^1.
|
|
50
|
-
"@logto/phrases-
|
|
51
|
-
"@logto/schemas": "workspace:1.
|
|
52
|
-
"@logto/shared": "workspace:^2.0.
|
|
53
|
-
"@silverhand/essentials": "^2.
|
|
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": "^
|
|
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": "^
|
|
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:
|
|
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
|
+
};
|