@logto/cli 1.6.0 → 1.8.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 +0 -6
- package/lib/commands/connector/utils.js +2 -2
- package/lib/commands/database/alteration/index.js +46 -30
- 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/tables.js +11 -9
- package/lib/commands/database/utils.d.ts +5 -1
- package/lib/commands/database/utils.js +8 -3
- package/lib/commands/install/utils.js +1 -1
- package/lib/commands/translate/index.js +2 -0
- package/lib/commands/translate/sync-keys/index.d.ts +11 -0
- package/lib/commands/translate/sync-keys/index.js +84 -0
- package/lib/commands/translate/sync-keys/utils.d.ts +56 -0
- package/lib/commands/translate/sync-keys/utils.js +249 -0
- package/lib/connector/utils.d.ts +3 -3
- package/lib/connector/utils.js +19 -6
- package/lib/package-json.d.ts +1 -1
- package/lib/package-json.js +10 -10
- package/package.json +10 -10
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.
|
|
112
|
+
const escapedFilename = filename.replaceAll('/', '-').replaceAll('@', '');
|
|
113
113
|
const tarPath = path.join(cwd, escapedFilename);
|
|
114
|
-
const packageDirectory = path.join(cwd, name.
|
|
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 });
|
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { createPoolFromConfig } from '../../../database.js';
|
|
4
4
|
import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/system.js';
|
|
5
5
|
import { consoleLog } from '../../../utils.js';
|
|
6
|
-
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
|
6
|
+
import { getAlterationFiles, getTimestampFromFilename, chooseRevertAlterationsByTimestamp, } from './utils.js';
|
|
7
7
|
import { chooseAlterationsByVersion, chooseRevertAlterationsByVersion } from './version.js';
|
|
8
8
|
const importAlterationScript = async (filePath) => {
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
@@ -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
|
|
16
|
+
const lastFile = files.at(-1);
|
|
17
17
|
if (!lastFile) {
|
|
18
18
|
return 0;
|
|
19
19
|
}
|
|
@@ -52,6 +52,15 @@ const deployAlteration = async (pool, { path: filePath, filename }, action = 'up
|
|
|
52
52
|
}
|
|
53
53
|
consoleLog.info(`Run alteration ${filename} \`${action}()\` function succeeded`);
|
|
54
54
|
};
|
|
55
|
+
const revertAlterations = async (alterations, pool) => {
|
|
56
|
+
consoleLog.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to revert`);
|
|
57
|
+
// The await inside the loop is intended, alterations should run in order
|
|
58
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
59
|
+
for (const alteration of alterations.slice().reverse()) {
|
|
60
|
+
// eslint-disable-next-line no-await-in-loop
|
|
61
|
+
await deployAlteration(pool, alteration, 'down');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
55
64
|
const alteration = {
|
|
56
65
|
command: ['alteration <action> [target]', 'alt', 'alter'],
|
|
57
66
|
describe: 'Perform database alteration',
|
|
@@ -66,37 +75,44 @@ const alteration = {
|
|
|
66
75
|
type: 'string',
|
|
67
76
|
}),
|
|
68
77
|
handler: async ({ action, target }) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
switch (action) {
|
|
79
|
+
case 'list': {
|
|
80
|
+
const files = await getAlterationFiles();
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
consoleLog.plain(file.filename);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
case 'deploy': {
|
|
87
|
+
const pool = await createPoolFromConfig();
|
|
88
|
+
const alterations = await chooseAlterationsByVersion(await getAvailableAlterations(pool), target);
|
|
89
|
+
consoleLog.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to deploy`);
|
|
90
|
+
// The await inside the loop is intended, alterations should run in order
|
|
91
|
+
for (const alteration of alterations) {
|
|
92
|
+
// eslint-disable-next-line no-await-in-loop
|
|
93
|
+
await deployAlteration(pool, alteration);
|
|
94
|
+
}
|
|
95
|
+
await pool.end();
|
|
96
|
+
break;
|
|
83
97
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await
|
|
98
|
+
case 'rollback':
|
|
99
|
+
case 'r': {
|
|
100
|
+
const pool = await createPoolFromConfig();
|
|
101
|
+
const alterations = await chooseRevertAlterationsByVersion(await getAvailableAlterations(pool, 'lte'), target ?? '');
|
|
102
|
+
await revertAlterations(alterations, pool);
|
|
103
|
+
await pool.end();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'rollback-to-timestamp': {
|
|
107
|
+
const pool = await createPoolFromConfig();
|
|
108
|
+
const alterations = await chooseRevertAlterationsByTimestamp(target ?? '');
|
|
109
|
+
await revertAlterations(alterations, pool);
|
|
110
|
+
await pool.end();
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
default: {
|
|
114
|
+
consoleLog.fatal('Unsupported action');
|
|
95
115
|
}
|
|
96
|
-
await pool.end();
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
consoleLog.fatal('Unsupported action');
|
|
100
116
|
}
|
|
101
117
|
},
|
|
102
118
|
};
|
|
@@ -2,3 +2,4 @@ import type { AlterationFile } from './type.js';
|
|
|
2
2
|
export declare const getTimestampFromFilename: (filename: string) => number;
|
|
3
3
|
export declare const getAlterationDirectory: () => string;
|
|
4
4
|
export declare const getAlterationFiles: () => Promise<AlterationFile[]>;
|
|
5
|
+
export declare const chooseRevertAlterationsByTimestamp: (target: string) => Promise<AlterationFile[]>;
|
|
@@ -36,3 +36,11 @@ export const getAlterationFiles = async () => {
|
|
|
36
36
|
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
|
37
37
|
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
|
38
38
|
};
|
|
39
|
+
export const chooseRevertAlterationsByTimestamp = async (target) => {
|
|
40
|
+
const files = await getAlterationFiles();
|
|
41
|
+
const targetTimestamp = Number(target);
|
|
42
|
+
if (Number.isNaN(targetTimestamp)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return files.filter(({ filename }) => getTimestampFromFilename(filename) > targetTimestamp);
|
|
46
|
+
};
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { createPoolFromConfig } from '../../database.js';
|
|
5
5
|
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config.js';
|
|
6
6
|
import { consoleLog } from '../../utils.js';
|
|
7
|
-
import { generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
|
|
7
|
+
import { PrivateKeyType, generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
|
|
8
8
|
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
|
|
9
9
|
const validateKeys = (keys) => {
|
|
10
10
|
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
|
|
@@ -19,6 +19,7 @@ const validRotateKeys = Object.freeze([
|
|
|
19
19
|
LogtoOidcConfigKey.PrivateKeys,
|
|
20
20
|
LogtoOidcConfigKey.CookieKeys,
|
|
21
21
|
]);
|
|
22
|
+
const validPrivateKeyTypes = Object.freeze([PrivateKeyType.RSA, PrivateKeyType.EC]);
|
|
22
23
|
const validateRotateKey = (key) => {
|
|
23
24
|
// Using `.includes()` will result a type error
|
|
24
25
|
// eslint-disable-next-line unicorn/prefer-includes
|
|
@@ -26,6 +27,13 @@ const validateRotateKey = (key) => {
|
|
|
26
27
|
consoleLog.fatal(`Invalid config key ${chalk.red(key)} found, expected one of ${validKeysDisplay}`);
|
|
27
28
|
}
|
|
28
29
|
};
|
|
30
|
+
const validatePrivateKeyType = (key) => {
|
|
31
|
+
// Using `.includes()` will result a type error
|
|
32
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
33
|
+
if (!validPrivateKeyTypes.some((element) => element === key)) {
|
|
34
|
+
consoleLog.fatal(`Invalid private key type ${chalk.red(key)} found, expected one of ${validPrivateKeyTypes.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
29
37
|
const getConfig = {
|
|
30
38
|
command: 'get <key> [keys...]',
|
|
31
39
|
describe: 'Get config value(s) of the given key(s) in Logto database',
|
|
@@ -103,9 +111,15 @@ const rotateConfig = {
|
|
|
103
111
|
describe: 'The tenant to operate',
|
|
104
112
|
type: 'string',
|
|
105
113
|
default: defaultTenantId,
|
|
114
|
+
})
|
|
115
|
+
.option('type', {
|
|
116
|
+
describe: `The key type for ${LogtoOidcConfigKey.PrivateKeys}, one of ${validPrivateKeyTypes.join(', ')}`,
|
|
117
|
+
type: 'string',
|
|
118
|
+
default: 'ec',
|
|
106
119
|
}),
|
|
107
|
-
handler: async ({ key, tenantId }) => {
|
|
120
|
+
handler: async ({ key, tenantId, type }) => {
|
|
108
121
|
validateRotateKey(key);
|
|
122
|
+
validatePrivateKeyType(type);
|
|
109
123
|
const pool = await createPoolFromConfig();
|
|
110
124
|
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
|
111
125
|
if (!rows[0]) {
|
|
@@ -117,7 +131,7 @@ const rotateConfig = {
|
|
|
117
131
|
// No need for default. It's already exhaustive
|
|
118
132
|
switch (key) {
|
|
119
133
|
case LogtoOidcConfigKey.PrivateKeys: {
|
|
120
|
-
return [await generateOidcPrivateKey(), ...original];
|
|
134
|
+
return [await generateOidcPrivateKey(type), ...original];
|
|
121
135
|
}
|
|
122
136
|
case LogtoOidcConfigKey.CookieKeys: {
|
|
123
137
|
return [generateOidcCookieKey(), ...original];
|
|
@@ -8,3 +8,11 @@ import type { CommonQueryMethods } from 'slonik';
|
|
|
8
8
|
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
9
9
|
*/
|
|
10
10
|
export declare const appendAdminConsoleRedirectUris: (pool: CommonQueryMethods) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Create Cloud Service M2M application for the tenant.
|
|
13
|
+
*
|
|
14
|
+
* This is a built-in M2M application for the tenant to communicate with the cloud service.
|
|
15
|
+
* - default
|
|
16
|
+
* - admin
|
|
17
|
+
*/
|
|
18
|
+
export declare const seedTenantCloudServiceApplication: (pool: CommonQueryMethods, tenantId: string) => Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { adminConsoleApplicationId, adminTenantId, defaultTenantId } from '@logto/schemas';
|
|
1
|
+
import { adminConsoleApplicationId, adminTenantId, defaultTenantId, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, createCloudConnectionConfig, AdminTenantRole, } from '@logto/schemas';
|
|
2
2
|
import { GlobalValues } from '@logto/shared';
|
|
3
3
|
import { appendPath } from '@silverhand/essentials';
|
|
4
4
|
import { sql } from 'slonik';
|
|
5
|
+
import { insertInto } from '../../../database.js';
|
|
5
6
|
import { consoleLog } from '../../../utils.js';
|
|
6
7
|
/**
|
|
7
8
|
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
@@ -32,3 +33,25 @@ export const appendAdminConsoleRedirectUris = async (pool) => {
|
|
|
32
33
|
`);
|
|
33
34
|
consoleLog.succeed('Appended initial Redirect URIs to Admin Console:', redirectUris.map(String));
|
|
34
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Create Cloud Service M2M application for the tenant.
|
|
38
|
+
*
|
|
39
|
+
* This is a built-in M2M application for the tenant to communicate with the cloud service.
|
|
40
|
+
* - default
|
|
41
|
+
* - admin
|
|
42
|
+
*/
|
|
43
|
+
export const seedTenantCloudServiceApplication = async (pool, tenantId) => {
|
|
44
|
+
// Create Cloud Service M2M application
|
|
45
|
+
const cloudServiceApplication = createTenantMachineToMachineApplication(tenantId);
|
|
46
|
+
await pool.query(insertInto(cloudServiceApplication, 'applications'));
|
|
47
|
+
// Assign tenantApplicationRole to Cloud Service M2M application
|
|
48
|
+
const { id: tenantApplicationRoleId } = await pool.one(sql `
|
|
49
|
+
select id from roles
|
|
50
|
+
where name = ${AdminTenantRole.TenantApplication}
|
|
51
|
+
and tenant_id = ${adminTenantId}
|
|
52
|
+
`);
|
|
53
|
+
await pool.query(insertInto(createAdminTenantApplicationRole(cloudServiceApplication.id, tenantApplicationRoleId), 'applications_roles'));
|
|
54
|
+
// Create Cloud Service M2M application logto_config
|
|
55
|
+
await pool.query(insertInto(createCloudConnectionConfig(tenantId, cloudServiceApplication.id, cloudServiceApplication.secret), 'logto_configs'));
|
|
56
|
+
consoleLog.succeed('Cloud Service Application successfully created for:', tenantId);
|
|
57
|
+
};
|
|
@@ -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,
|
|
3
|
+
import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, 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';
|
|
@@ -8,7 +8,7 @@ import { insertInto } from '../../../database.js';
|
|
|
8
8
|
import { getDatabaseName } from '../../../queries/database.js';
|
|
9
9
|
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
|
10
10
|
import { consoleLog, getPathInModule } from '../../../utils.js';
|
|
11
|
-
import { appendAdminConsoleRedirectUris } from './cloud.js';
|
|
11
|
+
import { appendAdminConsoleRedirectUris, seedTenantCloudServiceApplication } from './cloud.js';
|
|
12
12
|
import { seedOidcConfigs } from './oidc-config.js';
|
|
13
13
|
import { assignScopesToRole, createTenant, seedAdminData } from './tenant.js';
|
|
14
14
|
const getExplicitOrder = (query) => {
|
|
@@ -82,22 +82,21 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
|
82
82
|
await seedAdminData(connection, adminAdminData);
|
|
83
83
|
await seedAdminData(connection, createMeApiInAdminTenant());
|
|
84
84
|
const [cloudData, ...cloudAdditionalScopes] = createCloudApi();
|
|
85
|
-
const applicationRole = createTenantApplicationRole();
|
|
86
85
|
await seedAdminData(connection, cloudData, ...cloudAdditionalScopes);
|
|
86
|
+
// Create tenant application role
|
|
87
|
+
const applicationRole = createTenantApplicationRole();
|
|
87
88
|
await connection.query(insertInto(applicationRole, 'roles'));
|
|
88
89
|
await assignScopesToRole(connection, adminTenantId, applicationRole.id, ...cloudAdditionalScopes
|
|
89
90
|
.filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail)
|
|
90
91
|
.map(({ id }) => id));
|
|
91
|
-
// Add M2M app for default tenant
|
|
92
|
-
const defaultTenantApplication = createTenantMachineToMachineApplication(defaultTenantId);
|
|
93
|
-
await connection.query(insertInto(defaultTenantApplication, 'applications'));
|
|
94
|
-
await connection.query(insertInto(createAdminTenantApplicationRole(defaultTenantApplication.id, applicationRole.id), 'applications_roles'));
|
|
95
92
|
// Assign all cloud API scopes to role `admin:admin`
|
|
96
93
|
await assignScopesToRole(connection, adminTenantId, adminAdminData.role.id, ...cloudAdditionalScopes.map(({ id }) => id));
|
|
94
|
+
// FIXME: @wangsijie should not create tenant Cloud Service application in the OSS DB.
|
|
95
|
+
await seedTenantCloudServiceApplication(connection, defaultTenantId);
|
|
97
96
|
await Promise.all([
|
|
98
97
|
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
|
99
|
-
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
|
|
100
98
|
connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
|
|
99
|
+
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
|
|
101
100
|
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
|
|
102
101
|
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
|
103
102
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
@@ -105,5 +104,8 @@ export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
|
105
104
|
consoleLog.succeed('Seed data');
|
|
106
105
|
};
|
|
107
106
|
export const seedCloud = async (connection) => {
|
|
108
|
-
await
|
|
107
|
+
await Promise.all([
|
|
108
|
+
appendAdminConsoleRedirectUris(connection),
|
|
109
|
+
seedTenantCloudServiceApplication(connection, adminTenantId),
|
|
110
|
+
]);
|
|
109
111
|
};
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare enum PrivateKeyType {
|
|
2
|
+
RSA = "rsa",
|
|
3
|
+
EC = "ec"
|
|
4
|
+
}
|
|
5
|
+
export declare const generateOidcPrivateKey: (type?: PrivateKeyType) => Promise<string>;
|
|
2
6
|
export declare const generateOidcCookieKey: () => string;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { generateKeyPair } from 'node:crypto';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { generateStandardId } from '@logto/shared';
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export var PrivateKeyType;
|
|
5
|
+
(function (PrivateKeyType) {
|
|
6
|
+
PrivateKeyType["RSA"] = "rsa";
|
|
7
|
+
PrivateKeyType["EC"] = "ec";
|
|
8
|
+
})(PrivateKeyType || (PrivateKeyType = {}));
|
|
9
|
+
export const generateOidcPrivateKey = async (type = PrivateKeyType.EC) => {
|
|
10
|
+
if (type === PrivateKeyType.RSA) {
|
|
6
11
|
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
|
7
12
|
modulusLength: 4096,
|
|
8
13
|
publicKeyEncoding: {
|
|
@@ -17,7 +22,7 @@ export const generateOidcPrivateKey = async (type = 'ec') => {
|
|
|
17
22
|
return privateKey;
|
|
18
23
|
}
|
|
19
24
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
20
|
-
if (type ===
|
|
25
|
+
if (type === PrivateKeyType.EC) {
|
|
21
26
|
const { privateKey } = await promisify(generateKeyPair)('ec', {
|
|
22
27
|
// https://security.stackexchange.com/questions/78621/which-elliptic-curve-should-i-use
|
|
23
28
|
namedCurve: 'secp384r1',
|
|
@@ -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
|
|
64
|
+
const pgCurrent = semver.coerce(pgArray.at(-1));
|
|
65
65
|
return !pgCurrent || pgCurrent.compare(pgRequired) < 0;
|
|
66
66
|
},
|
|
67
67
|
});
|
|
@@ -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
|
};
|
|
@@ -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
|
+
};
|
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/package-json.d.ts
CHANGED
|
@@ -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;
|
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.8.0",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -48,7 +48,7 @@ export const packageJson = {
|
|
|
48
48
|
"@logto/language-kit": "workspace:^1.0.0",
|
|
49
49
|
"@logto/phrases": "workspace:^1.4.1",
|
|
50
50
|
"@logto/phrases-ui": "workspace:^1.2.0",
|
|
51
|
-
"@logto/schemas": "workspace:1.
|
|
51
|
+
"@logto/schemas": "workspace:1.8.0",
|
|
52
52
|
"@logto/shared": "workspace:^2.0.0",
|
|
53
53
|
"@silverhand/essentials": "^2.5.0",
|
|
54
54
|
"chalk": "^5.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",
|
|
@@ -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": "
|
|
77
|
-
"@silverhand/ts-config": "
|
|
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.
|
|
86
|
+
"eslint": "^8.44.0",
|
|
86
87
|
"jest": "^29.5.0",
|
|
87
|
-
"lint-staged": "^
|
|
88
|
-
"prettier": "^
|
|
89
|
-
"sinon": "^15.0.0"
|
|
90
|
-
"typescript": "^5.0.0"
|
|
88
|
+
"lint-staged": "^14.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.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@logto/language-kit": "^1.0.0",
|
|
34
34
|
"@logto/phrases": "^1.4.1",
|
|
35
35
|
"@logto/phrases-ui": "^1.2.0",
|
|
36
|
-
"@logto/schemas": "1.
|
|
36
|
+
"@logto/schemas": "1.8.0",
|
|
37
37
|
"@logto/shared": "^2.0.0",
|
|
38
38
|
"@silverhand/essentials": "^2.5.0",
|
|
39
39
|
"chalk": "^5.0.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"ora": "^6.1.2",
|
|
47
47
|
"p-limit": "^4.0.0",
|
|
48
48
|
"p-queue": "^7.3.4",
|
|
49
|
-
"p-retry": "^
|
|
49
|
+
"p-retry": "^6.0.0",
|
|
50
50
|
"pg-protocol": "^1.6.0",
|
|
51
51
|
"roarr": "^7.11.0",
|
|
52
52
|
"semver": "^7.3.8",
|
|
@@ -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": "
|
|
62
|
-
"@silverhand/ts-config": "
|
|
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.
|
|
71
|
+
"eslint": "^8.44.0",
|
|
71
72
|
"jest": "^29.5.0",
|
|
72
|
-
"lint-staged": "^
|
|
73
|
-
"prettier": "^
|
|
74
|
-
"sinon": "^15.0.0"
|
|
75
|
-
"typescript": "^5.0.0"
|
|
73
|
+
"lint-staged": "^14.0.0",
|
|
74
|
+
"prettier": "^3.0.0",
|
|
75
|
+
"sinon": "^15.0.0"
|
|
76
76
|
},
|
|
77
77
|
"eslintConfig": {
|
|
78
78
|
"extends": "@silverhand",
|