@nocobase/cli 3.0.0-alpha.1 → 3.0.0-alpha.12
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/dist/commands/api/resource/create.js +11 -2
- package/dist/commands/api/swagger/get.js +96 -0
- package/dist/commands/api/swagger/index.js +20 -0
- package/dist/commands/api/swagger/list.js +92 -0
- package/dist/commands/env/add.js +6 -0
- package/dist/commands/env/update.js +13 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/install.js +18 -62
- package/dist/commands/portal/config.js +16 -5
- package/dist/commands/portal/create.js +17 -26
- package/dist/commands/portal/destroy.js +28 -6
- package/dist/commands/portal/dev.js +2 -0
- package/dist/commands/portal/list.js +5 -5
- package/dist/commands/portal/pull.js +32 -3
- package/dist/lib/api-client.js +35 -9
- package/dist/lib/app-client-entry-mode.js +28 -0
- package/dist/lib/app-managed-resources.js +2 -0
- package/dist/lib/auth-store.js +52 -1
- package/dist/lib/bootstrap.js +3 -1
- package/dist/lib/browser.js +29 -0
- package/dist/lib/env-auth.js +2 -36
- package/dist/lib/env-command-config.js +1 -0
- package/dist/lib/env-config.js +10 -2
- package/dist/lib/env-portal-config.js +30 -0
- package/dist/lib/env-proxy.js +25 -4
- package/dist/lib/generated-command.js +81 -0
- package/dist/lib/managed-env-file.js +67 -13
- package/dist/lib/managed-init-env.js +0 -2
- package/dist/lib/plugin-import.js +30 -8
- package/dist/lib/portal-build-html.js +27 -0
- package/dist/lib/portal-config.js +6 -20
- package/dist/lib/portal-configure.js +49 -56
- package/dist/lib/portal-create.js +96 -14
- package/dist/lib/portal-deploy.js +30 -47
- package/dist/lib/portal-destroy.js +34 -20
- package/dist/lib/portal-dev.js +6 -7
- package/dist/lib/portal-env-files.js +2 -1
- package/dist/lib/portal-info.js +2 -5
- package/dist/lib/portal-list.js +20 -26
- package/dist/lib/portal-path-safety.js +76 -0
- package/dist/lib/portal-source.js +227 -58
- package/dist/lib/prompt-catalog-core.js +2 -2
- package/dist/lib/prompt-catalog-terminal.js +4 -5
- package/dist/lib/prompt-web-ui.js +12 -6
- package/dist/lib/resource-command.js +18 -2
- package/dist/lib/resource-request.js +8 -0
- package/dist/lib/run-npm.js +68 -4
- package/dist/lib/runtime-generator.js +28 -1
- package/dist/lib/swagger-command.js +52 -0
- package/dist/locale/en-US.json +81 -15
- package/dist/locale/zh-CN.json +81 -15
- package/package.json +2 -2
package/dist/lib/env-config.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
import { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
|
|
9
10
|
import { normalizeEnvProxyConfig } from './env-proxy-config.js';
|
|
10
11
|
import { resolveAppPublicPath } from './app-public-path.js';
|
|
12
|
+
import { normalizeEnvPortalsConfig } from './env-portal-config.js';
|
|
11
13
|
const STRING_ENV_CONFIG_KEYS = [
|
|
12
14
|
'source',
|
|
13
15
|
'downloadVersion',
|
|
@@ -36,8 +38,6 @@ const STRING_ENV_CONFIG_KEYS = [
|
|
|
36
38
|
'dbSchema',
|
|
37
39
|
'dbTablePrefix',
|
|
38
40
|
'lang',
|
|
39
|
-
'portalType',
|
|
40
|
-
'portalName',
|
|
41
41
|
'portalTemplate',
|
|
42
42
|
'rootUsername',
|
|
43
43
|
'rootEmail',
|
|
@@ -86,6 +86,10 @@ export function buildStoredEnvConfig(input) {
|
|
|
86
86
|
envConfig[key] = key === 'appPublicPath' ? resolveAppPublicPath(value) : value;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
const appClientEntryMode = normalizeAppClientEntryMode(input.appClientEntryMode);
|
|
90
|
+
if (appClientEntryMode) {
|
|
91
|
+
envConfig.appClientEntryMode = appClientEntryMode;
|
|
92
|
+
}
|
|
89
93
|
const setupState = resolveSetupState(input.setupState);
|
|
90
94
|
if (setupState) {
|
|
91
95
|
envConfig.setupState = setupState;
|
|
@@ -118,5 +122,9 @@ export function buildStoredEnvConfig(input) {
|
|
|
118
122
|
if (proxy) {
|
|
119
123
|
envConfig.proxy = proxy;
|
|
120
124
|
}
|
|
125
|
+
const portals = normalizeEnvPortalsConfig(input.portals);
|
|
126
|
+
if (portals) {
|
|
127
|
+
envConfig.portals = portals;
|
|
128
|
+
}
|
|
121
129
|
return envConfig;
|
|
122
130
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
function trimValue(value) {
|
|
10
|
+
return String(value ?? '').trim();
|
|
11
|
+
}
|
|
12
|
+
function readRecord(value) {
|
|
13
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
|
+
}
|
|
15
|
+
export function normalizeEnvPortalsConfig(value) {
|
|
16
|
+
const input = readRecord(value);
|
|
17
|
+
const portals = {};
|
|
18
|
+
for (const [portal, rawEntry] of Object.entries(input)) {
|
|
19
|
+
const portalName = trimValue(portal);
|
|
20
|
+
if (!portalName) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const entry = readRecord(rawEntry);
|
|
24
|
+
const portalPath = trimValue(entry.path);
|
|
25
|
+
if (portalPath) {
|
|
26
|
+
portals[portalName] = { path: portalPath };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(portals).length > 0 ? portals : undefined;
|
|
30
|
+
}
|
package/dist/lib/env-proxy.js
CHANGED
|
@@ -22,7 +22,7 @@ const DEFAULT_PLUGIN_STATICS_PATH = '/static/plugins/';
|
|
|
22
22
|
const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
23
23
|
const SETTINGS_CLIENT_PREFIX = 'settings';
|
|
24
24
|
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
|
25
|
-
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
|
25
|
+
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only', 'settings-default']);
|
|
26
26
|
const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
|
|
27
27
|
const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
|
|
28
28
|
const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
|
|
@@ -605,17 +605,30 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
605
605
|
` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
|
|
606
606
|
].join('\n');
|
|
607
607
|
}
|
|
608
|
-
function
|
|
608
|
+
function buildPortalRootPublicPath(appPublicPath) {
|
|
609
609
|
return appPublicPath === DEFAULT_APP_PUBLIC_PATH
|
|
610
610
|
? `/${PORTAL_CLIENT_PREFIX}/`
|
|
611
611
|
: `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
|
|
612
612
|
}
|
|
613
613
|
function buildNginxPortalLocationBlock(context) {
|
|
614
|
-
const portalBasePath = trimTrailingSlash(
|
|
614
|
+
const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
|
|
615
615
|
const portalBasePathPattern = escapeRegExp(portalBasePath);
|
|
616
616
|
return [
|
|
617
|
+
` location = ${portalBasePath} {`,
|
|
618
|
+
' absolute_redirect off;',
|
|
619
|
+
` return 302 ${context.v2PublicPath}$is_args$args;`,
|
|
620
|
+
' }',
|
|
621
|
+
'',
|
|
622
|
+
` location = ${portalBasePath}/ {`,
|
|
623
|
+
' absolute_redirect off;',
|
|
624
|
+
` return 302 ${context.v2PublicPath}$is_args$args;`,
|
|
625
|
+
' }',
|
|
626
|
+
'',
|
|
617
627
|
` location ^~ ${portalBasePath}/apps/ {`,
|
|
618
628
|
' absolute_redirect off;',
|
|
629
|
+
` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {`,
|
|
630
|
+
` return 302 ${context.v2PublicPath}apps/$subapp/$is_args$args;`,
|
|
631
|
+
' }',
|
|
619
632
|
'',
|
|
620
633
|
` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
|
|
621
634
|
` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
|
|
@@ -640,7 +653,6 @@ function buildNginxPortalLocationBlock(context) {
|
|
|
640
653
|
'',
|
|
641
654
|
` location ^~ ${portalBasePath}/ {`,
|
|
642
655
|
' absolute_redirect off;',
|
|
643
|
-
'',
|
|
644
656
|
` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
|
|
645
657
|
` return 308 ${portalBasePath}/$portal/$is_args$args;`,
|
|
646
658
|
' }',
|
|
@@ -1229,6 +1241,7 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1229
1241
|
const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
|
|
1230
1242
|
const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
|
|
1231
1243
|
const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
|
|
1244
|
+
const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
|
|
1232
1245
|
const rootRedirectBlock = context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
|
|
1233
1246
|
? ''
|
|
1234
1247
|
: `
|
|
@@ -1321,6 +1334,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1321
1334
|
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1322
1335
|
' }',
|
|
1323
1336
|
'',
|
|
1337
|
+
` handle ${portalBasePath} {`,
|
|
1338
|
+
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1339
|
+
' }',
|
|
1340
|
+
'',
|
|
1341
|
+
` handle ${portalBasePath}/* {`,
|
|
1342
|
+
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1343
|
+
' }',
|
|
1344
|
+
'',
|
|
1324
1345
|
` @settingsRoute path_regexp settingsRoute ${settingsRoutePattern}`,
|
|
1325
1346
|
' handle @settingsRoute {',
|
|
1326
1347
|
` root * ${publicDir}`,
|
|
@@ -15,12 +15,20 @@
|
|
|
15
15
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
16
|
*/
|
|
17
17
|
import { Command, Flags } from '@oclif/core';
|
|
18
|
+
import { getCurrentEnvName, getEnv } from './auth-store.js';
|
|
18
19
|
import { executeApiRequest } from './api-client.js';
|
|
20
|
+
import { resolveAppUrlFromApiBaseUrl } from '../commands/env/shared.js';
|
|
19
21
|
import { findApiCommandCompatViolation, formatApiCommandCompatViolation } from './api-command-compat.js';
|
|
22
|
+
import { openUrlInDefaultBrowser } from './browser.js';
|
|
20
23
|
import { ensureCrossEnvConfirmed } from './env-guard.js';
|
|
21
24
|
import { applyPostProcessor } from './post-processors.js';
|
|
22
25
|
import { readInstalledManagedSkillsVersion } from './skills-manager.js';
|
|
23
26
|
import { registerPostProcessors } from '../post-processors/index.js';
|
|
27
|
+
const UI_OPERATION_QUERY_KEY = '_operation_';
|
|
28
|
+
const UI_OPERATION_VERSION = 1;
|
|
29
|
+
function encodeUIOperation(operation) {
|
|
30
|
+
return Buffer.from(JSON.stringify(operation), 'utf8').toString('base64url');
|
|
31
|
+
}
|
|
24
32
|
function buildParameterFlag(parameter, options) {
|
|
25
33
|
const hints = [parameter.in];
|
|
26
34
|
if (parameter.isFile) {
|
|
@@ -104,6 +112,13 @@ export function createGeneratedFlags(operation) {
|
|
|
104
112
|
required: true,
|
|
105
113
|
});
|
|
106
114
|
}
|
|
115
|
+
if (operation.ui) {
|
|
116
|
+
flags.ui = Flags.boolean({
|
|
117
|
+
description: 'Open the corresponding page in the NocoBase UI',
|
|
118
|
+
default: false,
|
|
119
|
+
helpGroup: 'Global',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
107
122
|
flags['api-base-url'] = Flags.string({
|
|
108
123
|
description: 'NocoBase API base URL, for example http://localhost:13000/api',
|
|
109
124
|
helpGroup: 'Global',
|
|
@@ -142,6 +157,68 @@ export function createGeneratedFlags(operation) {
|
|
|
142
157
|
});
|
|
143
158
|
return flags;
|
|
144
159
|
}
|
|
160
|
+
function hasFlagValue(value) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.length > 0;
|
|
163
|
+
}
|
|
164
|
+
return value !== undefined && value !== '';
|
|
165
|
+
}
|
|
166
|
+
function listProvidedBodyFlags(flags, operation) {
|
|
167
|
+
const rawBodyFlags = ['body', 'body-file'].filter((flagName) => hasFlagValue(flags[flagName])).map((flagName) => `--${flagName}`);
|
|
168
|
+
const uiParameterNames = new Set(operation.ui?.parameters ?? []);
|
|
169
|
+
const bodyFieldFlags = operation.parameters
|
|
170
|
+
.filter((parameter) => parameter.in === 'body' && !uiParameterNames.has(parameter.name) && hasFlagValue(flags[parameter.flagName]))
|
|
171
|
+
.map((parameter) => `--${parameter.flagName}`);
|
|
172
|
+
return [...rawBodyFlags, ...bodyFieldFlags];
|
|
173
|
+
}
|
|
174
|
+
async function resolveUiAppUrl(flags) {
|
|
175
|
+
const apiBaseUrl = typeof flags['api-base-url'] === 'string' ? flags['api-base-url'] : undefined;
|
|
176
|
+
if (apiBaseUrl) {
|
|
177
|
+
return resolveAppUrlFromApiBaseUrl(apiBaseUrl);
|
|
178
|
+
}
|
|
179
|
+
const requestedEnv = typeof flags.env === 'string' ? flags.env : undefined;
|
|
180
|
+
const envName = requestedEnv ?? (await getCurrentEnvName());
|
|
181
|
+
const env = await getEnv(envName);
|
|
182
|
+
if (!env?.baseUrl) {
|
|
183
|
+
throw new Error(env
|
|
184
|
+
? `Env "${envName}" is missing a base URL. Use --api-base-url or update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\` first.`
|
|
185
|
+
: `Env "${envName}" is not configured. Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`);
|
|
186
|
+
}
|
|
187
|
+
return resolveAppUrlFromApiBaseUrl(env.baseUrl);
|
|
188
|
+
}
|
|
189
|
+
function buildUiOperationUrl(appUrl, path, encodedOperation) {
|
|
190
|
+
const url = new URL(appUrl);
|
|
191
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/${path}`;
|
|
192
|
+
url.searchParams.set(UI_OPERATION_QUERY_KEY, encodedOperation);
|
|
193
|
+
return url.toString();
|
|
194
|
+
}
|
|
195
|
+
async function openUiOperation(command, operation, flags) {
|
|
196
|
+
const { operationId, ui } = operation;
|
|
197
|
+
if (!ui || !operationId) {
|
|
198
|
+
command.error('This API operation does not support --ui.');
|
|
199
|
+
}
|
|
200
|
+
const bodyFlags = listProvidedBodyFlags(flags, operation);
|
|
201
|
+
if (bodyFlags.length) {
|
|
202
|
+
command.error('--ui cannot be combined with API request body flags. Remove --ui to submit through the API, or remove the body flags to open the UI.');
|
|
203
|
+
}
|
|
204
|
+
const uiParameterNames = new Set(ui.parameters ?? []);
|
|
205
|
+
const params = Object.fromEntries(operation.parameters
|
|
206
|
+
.filter((parameter) => uiParameterNames.has(parameter.name) && hasFlagValue(flags[parameter.flagName]))
|
|
207
|
+
.map((parameter) => [parameter.name, flags[parameter.flagName]]));
|
|
208
|
+
const uiOperation = {
|
|
209
|
+
v: UI_OPERATION_VERSION,
|
|
210
|
+
operationId,
|
|
211
|
+
...(Object.keys(params).length ? { params } : {}),
|
|
212
|
+
};
|
|
213
|
+
const encodedOperation = encodeUIOperation(uiOperation);
|
|
214
|
+
const appUrl = await resolveUiAppUrl(flags);
|
|
215
|
+
const targetUrl = buildUiOperationUrl(appUrl, ui.path, encodedOperation);
|
|
216
|
+
const opened = await openUrlInDefaultBrowser(targetUrl);
|
|
217
|
+
command.log(targetUrl);
|
|
218
|
+
if (!opened) {
|
|
219
|
+
command.warn('Could not open the default browser. Copy the URL above to continue.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
145
222
|
export class GeneratedApiCommand extends Command {
|
|
146
223
|
static operation;
|
|
147
224
|
static runtimeVersion;
|
|
@@ -169,6 +246,10 @@ export class GeneratedApiCommand extends Command {
|
|
|
169
246
|
if (compatViolation) {
|
|
170
247
|
this.error(formatApiCommandCompatViolation(compatViolation));
|
|
171
248
|
}
|
|
249
|
+
if (flags.ui) {
|
|
250
|
+
await openUiOperation(this, ctor.operation, flags);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
172
253
|
const response = await executeApiRequest({
|
|
173
254
|
cliVersion,
|
|
174
255
|
skillsVersion,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
|
|
11
12
|
import { resolveEnvKind } from './auth-store.js';
|
|
12
13
|
import { resolveConfiguredEnvPath } from './cli-home.js';
|
|
13
14
|
import { resolveDockerEnvFileArg, resolveDockerEnvFilePath } from "./docker-env-file.js";
|
|
@@ -17,13 +18,18 @@ export const DEFAULT_MANAGED_ENV_FILE_VALUES = {
|
|
|
17
18
|
APP_PROCESS_ADAPTER: 'local',
|
|
18
19
|
APP_CLIENT_ENTRY_MODE: 'modern-only',
|
|
19
20
|
};
|
|
21
|
+
function buildManagedEnvFileDefaults(config, defaults = DEFAULT_MANAGED_ENV_FILE_VALUES) {
|
|
22
|
+
return {
|
|
23
|
+
...defaults,
|
|
24
|
+
APP_CLIENT_ENTRY_MODE: normalizeAppClientEntryMode(config?.appClientEntryMode) ??
|
|
25
|
+
trimValue(defaults.APP_CLIENT_ENTRY_MODE) ??
|
|
26
|
+
DEFAULT_MANAGED_ENV_FILE_VALUES.APP_CLIENT_ENTRY_MODE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
20
29
|
function trimValue(value) {
|
|
21
30
|
const text = String(value ?? '').trim();
|
|
22
31
|
return text || undefined;
|
|
23
32
|
}
|
|
24
|
-
function normalizeEnvFilePath(value) {
|
|
25
|
-
return value.replace(/\\/g, '/');
|
|
26
|
-
}
|
|
27
33
|
function stripWrappingQuotes(value) {
|
|
28
34
|
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
29
35
|
return value
|
|
@@ -39,6 +45,29 @@ function stripWrappingQuotes(value) {
|
|
|
39
45
|
}
|
|
40
46
|
return value;
|
|
41
47
|
}
|
|
48
|
+
function escapeRegExp(value) {
|
|
49
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
function upsertSimpleEnvContent(content, values) {
|
|
52
|
+
let nextContent = content;
|
|
53
|
+
const missingEntries = new Map(Object.entries(values).filter(([, value]) => trimValue(value)));
|
|
54
|
+
for (const key of Array.from(missingEntries.keys())) {
|
|
55
|
+
const pattern = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=.*$`, 'gm');
|
|
56
|
+
if (!pattern.test(nextContent)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
nextContent = nextContent.replace(pattern, `${key}=${values[key]}`);
|
|
60
|
+
missingEntries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
if (missingEntries.size === 0) {
|
|
63
|
+
return nextContent;
|
|
64
|
+
}
|
|
65
|
+
const separator = nextContent && !nextContent.endsWith('\n') ? '\n' : '';
|
|
66
|
+
const appended = Array.from(missingEntries.entries())
|
|
67
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
return `${nextContent}${separator}${appended}\n`;
|
|
70
|
+
}
|
|
42
71
|
export function parseSimpleEnvFile(content) {
|
|
43
72
|
const values = {};
|
|
44
73
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
@@ -63,38 +92,39 @@ export function resolveManagedLocalEnvFilePath(runtime) {
|
|
|
63
92
|
const config = runtime.env.config ?? {};
|
|
64
93
|
const explicitEnvFile = trimValue(config.envFile);
|
|
65
94
|
if (explicitEnvFile) {
|
|
66
|
-
return
|
|
95
|
+
return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
|
|
67
96
|
}
|
|
68
97
|
const configuredAppPath = resolveConfiguredAppPath(config);
|
|
69
98
|
if (configuredAppPath) {
|
|
70
|
-
return
|
|
99
|
+
return path.join(configuredAppPath, '.env');
|
|
71
100
|
}
|
|
72
101
|
if (path.basename(runtime.projectRoot) === 'source') {
|
|
73
|
-
return
|
|
102
|
+
return path.resolve(runtime.projectRoot, '..', '.env');
|
|
74
103
|
}
|
|
75
|
-
return
|
|
104
|
+
return path.join(runtime.projectRoot, '.env');
|
|
76
105
|
}
|
|
77
106
|
export function resolveManagedEnvFilePathFromConfig(envName, config) {
|
|
78
107
|
const kind = config?.kind ?? resolveEnvKind(config);
|
|
79
108
|
if (kind === 'docker') {
|
|
80
|
-
|
|
81
|
-
return filePath ? normalizeEnvFilePath(filePath) : undefined;
|
|
109
|
+
return resolveDockerEnvFilePath(envName, config);
|
|
82
110
|
}
|
|
83
111
|
if (kind !== 'local') {
|
|
84
112
|
return undefined;
|
|
85
113
|
}
|
|
86
114
|
const explicitEnvFile = trimValue(config?.envFile);
|
|
87
115
|
if (explicitEnvFile) {
|
|
88
|
-
return
|
|
116
|
+
return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
|
|
89
117
|
}
|
|
90
118
|
const configuredAppPath = resolveConfiguredAppPath(config);
|
|
91
119
|
if (configuredAppPath) {
|
|
92
|
-
return
|
|
120
|
+
return path.join(configuredAppPath, '.env');
|
|
93
121
|
}
|
|
94
122
|
const configuredAppRootPath = trimValue(config?.appRootPath);
|
|
95
123
|
if (configuredAppRootPath) {
|
|
96
124
|
const appRootPath = resolveConfiguredEnvPath(configuredAppRootPath) ?? configuredAppRootPath;
|
|
97
|
-
return
|
|
125
|
+
return path.basename(appRootPath) === 'source'
|
|
126
|
+
? path.resolve(appRootPath, '..', '.env')
|
|
127
|
+
: path.join(appRootPath, '.env');
|
|
98
128
|
}
|
|
99
129
|
return undefined;
|
|
100
130
|
}
|
|
@@ -114,7 +144,8 @@ export async function ensureManagedEnvFileDefaults(envName, config, defaults = D
|
|
|
114
144
|
}
|
|
115
145
|
}
|
|
116
146
|
const existing = parseSimpleEnvFile(content);
|
|
117
|
-
const
|
|
147
|
+
const resolvedDefaults = buildManagedEnvFileDefaults(config, defaults);
|
|
148
|
+
const missingEntries = Object.entries(resolvedDefaults).filter(([key, value]) => trimValue(value) && !existing[key]);
|
|
118
149
|
if (missingEntries.length === 0) {
|
|
119
150
|
return envFilePath;
|
|
120
151
|
}
|
|
@@ -124,6 +155,29 @@ export async function ensureManagedEnvFileDefaults(envName, config, defaults = D
|
|
|
124
155
|
await writeFile(envFilePath, nextContent, 'utf8');
|
|
125
156
|
return envFilePath;
|
|
126
157
|
}
|
|
158
|
+
export async function upsertManagedEnvFileValues(envName, config, values) {
|
|
159
|
+
const envFilePath = resolveManagedEnvFilePathFromConfig(envName, config);
|
|
160
|
+
if (!envFilePath) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
let content = '';
|
|
164
|
+
try {
|
|
165
|
+
content = await readFile(envFilePath, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
169
|
+
if (code !== 'ENOENT') {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const nextContent = upsertSimpleEnvContent(content, values);
|
|
174
|
+
if (nextContent === content) {
|
|
175
|
+
return envFilePath;
|
|
176
|
+
}
|
|
177
|
+
await mkdir(path.dirname(envFilePath), { recursive: true });
|
|
178
|
+
await writeFile(envFilePath, nextContent, 'utf8');
|
|
179
|
+
return envFilePath;
|
|
180
|
+
}
|
|
127
181
|
export async function resolveManagedRuntimeEnvFilePath(runtime) {
|
|
128
182
|
if (runtime.kind === 'local') {
|
|
129
183
|
return resolveManagedLocalEnvFilePath(runtime);
|
|
@@ -29,8 +29,6 @@ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
|
|
|
29
29
|
put('INIT_ROOT_PASSWORD', config?.rootPassword);
|
|
30
30
|
put('INIT_ROOT_NICKNAME', config?.rootNickname);
|
|
31
31
|
if (options.includePortal !== false) {
|
|
32
|
-
put('INIT_PORTAL_TYPE', config?.portalType);
|
|
33
|
-
put('INIT_PORTAL_NAME', config?.portalName);
|
|
34
32
|
put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
|
|
35
33
|
}
|
|
36
34
|
return out;
|
|
@@ -207,6 +207,29 @@ async function openPluginSource(source, npmRegistry, runFn = run) {
|
|
|
207
207
|
}
|
|
208
208
|
return await packNpmPluginSource(source, npmRegistry, runFn);
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Locate the directory holding the plugin's `package.json` inside a freshly extracted archive.
|
|
212
|
+
*
|
|
213
|
+
* Archives reach us in two shapes: npm-style tarballs (`npm pack`, registry downloads) wrap everything in a single
|
|
214
|
+
* top-level directory — conventionally `package/` — while NocoBase's own `yarn build <plugin> --tar` writes entries at
|
|
215
|
+
* the archive root. Extracting with a fixed `strip: 1` would silently discard the root-level files of the latter, so we
|
|
216
|
+
* extract verbatim and pick the package root here instead. When neither shape matches, return the extract root and let
|
|
217
|
+
* `readPluginMetadata` report the missing `package.json`.
|
|
218
|
+
*/
|
|
219
|
+
async function resolveArchivePackageRoot(extractRoot) {
|
|
220
|
+
if (await pathExists(path.join(extractRoot, 'package.json'))) {
|
|
221
|
+
return extractRoot;
|
|
222
|
+
}
|
|
223
|
+
const entries = await fsp.readdir(extractRoot, { withFileTypes: true });
|
|
224
|
+
const directories = entries.filter((entry) => entry.isDirectory());
|
|
225
|
+
if (directories.length === 1) {
|
|
226
|
+
const nestedRoot = path.join(extractRoot, directories[0].name);
|
|
227
|
+
if (await pathExists(path.join(nestedRoot, 'package.json'))) {
|
|
228
|
+
return nestedRoot;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return extractRoot;
|
|
232
|
+
}
|
|
210
233
|
async function readPluginMetadata(extractRoot, sourceLabel) {
|
|
211
234
|
const packageJsonPath = path.join(extractRoot, 'package.json');
|
|
212
235
|
let content;
|
|
@@ -241,22 +264,21 @@ export async function importPluginSource(source, options = {}) {
|
|
|
241
264
|
await fsp.mkdir(storagePluginsPath, { recursive: true });
|
|
242
265
|
const archive = await openPluginSource(normalizedSource, options.npmRegistry, options.runFn);
|
|
243
266
|
const stageDir = await fsp.mkdtemp(path.join(storagePluginsPath, '.nb-plugin-import-'));
|
|
244
|
-
let stageMoved = false;
|
|
245
267
|
try {
|
|
246
268
|
try {
|
|
247
|
-
await pipeline(archive.stream, createGunzip(), tar.extract({ cwd: stageDir
|
|
269
|
+
await pipeline(archive.stream, createGunzip(), tar.extract({ cwd: stageDir }));
|
|
248
270
|
}
|
|
249
271
|
catch (error) {
|
|
250
272
|
const message = error instanceof Error ? error.message : String(error);
|
|
251
273
|
throw new Error(`Failed to extract plugin archive from ${archive.source}: ${message}`);
|
|
252
274
|
}
|
|
253
|
-
const
|
|
275
|
+
const packageRoot = await resolveArchivePackageRoot(stageDir);
|
|
276
|
+
const { packageName, packageVersion } = await readPluginMetadata(packageRoot, archive.source);
|
|
254
277
|
const outputDir = resolvePluginOutputDir(storagePluginsPath, packageName);
|
|
255
278
|
const action = (await pathExists(outputDir)) ? 'updated' : 'installed';
|
|
256
279
|
await fsp.mkdir(path.dirname(outputDir), { recursive: true });
|
|
257
280
|
await fsp.rm(outputDir, { recursive: true, force: true });
|
|
258
|
-
await fsp.rename(
|
|
259
|
-
stageMoved = true;
|
|
281
|
+
await fsp.rename(packageRoot, outputDir);
|
|
260
282
|
return {
|
|
261
283
|
action,
|
|
262
284
|
packageName,
|
|
@@ -268,9 +290,9 @@ export async function importPluginSource(source, options = {}) {
|
|
|
268
290
|
};
|
|
269
291
|
}
|
|
270
292
|
finally {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
293
|
+
// Always removes the staging directory: it is either untouched (failure), or an empty wrapper left behind after the
|
|
294
|
+
// nested package root was renamed out of it.
|
|
295
|
+
await fsp.rm(stageDir, { recursive: true, force: true });
|
|
274
296
|
await archive.cleanup();
|
|
275
297
|
}
|
|
276
298
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
const BUILD_HTML_SCRIPT_PATH = path.join('scripts', 'build-html.mjs');
|
|
12
|
+
const BUILD_HTML_ENV_FILES_PATTERN = /return\s+\[\s*["']\.env["']\s*,\s*["']\.env\.local["']\s*,\s*`\.env\.\$\{mode\}`\s*,\s*`\.env\.\$\{mode\}\.local`\s*\]\.map\(\s*\(?file\)?\s*=>\s*path\.join\(rootDir,\s*file\)\s*\);/m;
|
|
13
|
+
const BUILD_HTML_ENV_ONLY_REPLACEMENT = 'return [".env"].map((file) => path.join(rootDir, file));';
|
|
14
|
+
export async function ensurePortalBuildHtmlReadsEnvOnly(portalDir) {
|
|
15
|
+
const scriptPath = path.join(portalDir, BUILD_HTML_SCRIPT_PATH);
|
|
16
|
+
let content;
|
|
17
|
+
try {
|
|
18
|
+
content = await readFile(scriptPath, 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const nextContent = content.replace(BUILD_HTML_ENV_FILES_PATTERN, BUILD_HTML_ENV_ONLY_REPLACEMENT);
|
|
24
|
+
if (nextContent !== content) {
|
|
25
|
+
await writeFile(scriptPath, nextContent, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import { executeApiRequest } from './api-client.js';
|
|
12
11
|
import { translateCli } from './cli-locale.js';
|
|
@@ -19,9 +18,10 @@ const UPDATE_PORTAL_OPERATION = {
|
|
|
19
18
|
bodyRequired: true,
|
|
20
19
|
parameters: [
|
|
21
20
|
{
|
|
22
|
-
name: '
|
|
23
|
-
flagName: '
|
|
21
|
+
name: 'filter',
|
|
22
|
+
flagName: 'filter',
|
|
24
23
|
in: 'query',
|
|
24
|
+
type: 'object',
|
|
25
25
|
required: true,
|
|
26
26
|
},
|
|
27
27
|
],
|
|
@@ -98,29 +98,15 @@ export function mergePortalConfigIntoOptions(config, currentOptions) {
|
|
|
98
98
|
}
|
|
99
99
|
return nextOptions;
|
|
100
100
|
}
|
|
101
|
-
export async function readPortalConfig(portalDir) {
|
|
102
|
-
const configPath = path.join(portalDir, 'portal.config.json');
|
|
103
|
-
const data = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
104
|
-
const config = readObject(data);
|
|
105
|
-
const git = readObject(config.git);
|
|
106
|
-
return buildPortalConfig({
|
|
107
|
-
portal: path.basename(portalDir),
|
|
108
|
-
sourceStorage: trimValue(config.sourceStorage),
|
|
109
|
-
gitRepo: trimValue(git.repo),
|
|
110
|
-
gitBranch: trimValue(git.branch),
|
|
111
|
-
gitPath: trimValue(git.path),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
export async function writePortalConfig(portalDir, config) {
|
|
115
|
-
await writeFile(path.join(portalDir, 'portal.config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
116
|
-
}
|
|
117
101
|
export async function syncPortalConfigToRemote(options) {
|
|
118
102
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
119
103
|
const response = await apiRequest({
|
|
120
104
|
cliVersion: options.cliVersion ?? '',
|
|
121
105
|
envName: options.envName,
|
|
122
106
|
flags: {
|
|
123
|
-
|
|
107
|
+
filter: {
|
|
108
|
+
portalName: options.portal,
|
|
109
|
+
},
|
|
124
110
|
body: JSON.stringify({
|
|
125
111
|
options: mergePortalConfigIntoOptions(options.config, options.currentOptions),
|
|
126
112
|
}),
|