@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
|
@@ -6,43 +6,23 @@
|
|
|
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 { mkdir, stat } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
9
|
import { translateCli } from './cli-locale.js';
|
|
12
|
-
import {
|
|
13
|
-
import { buildPortalConfig, buildPortalConfigFromOptions,
|
|
10
|
+
import { resolvePortalAppContext, resolvePortalSourcePath, resolveSavedPortalSourcePath, validatePortalSlug, } from './portal-create.js';
|
|
11
|
+
import { buildPortalConfig, buildPortalConfigFromOptions, syncPortalConfigToRemote, } from './portal-config.js';
|
|
14
12
|
import { findPortalListItem } from './portal-info.js';
|
|
15
13
|
import { listPortalWorkspaces } from './portal-list.js';
|
|
16
14
|
const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
|
|
17
15
|
function trimValue(value) {
|
|
18
16
|
return String(value ?? '').trim();
|
|
19
17
|
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await stat(target);
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function hasConfigurationChange(options) {
|
|
18
|
+
function hasSourceConfigurationChange(options) {
|
|
30
19
|
return Boolean(options.sourceStorage !== undefined ||
|
|
31
20
|
trimValue(options.gitRepo) ||
|
|
32
21
|
trimValue(options.gitBranch) ||
|
|
33
22
|
trimValue(options.gitPath));
|
|
34
23
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return await readPortalConfig(portalDir);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
const code = error.code;
|
|
41
|
-
if (code === 'ENOENT') {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
24
|
+
function hasPathConfigurationChange(options) {
|
|
25
|
+
return Boolean(trimValue(options.sourcePath));
|
|
46
26
|
}
|
|
47
27
|
function buildConfigFromRemoteOptions(params) {
|
|
48
28
|
if (params.options && Object.keys(params.options).length > 0) {
|
|
@@ -60,34 +40,49 @@ function buildConfigFromRemoteOptions(params) {
|
|
|
60
40
|
});
|
|
61
41
|
}
|
|
62
42
|
export async function configurePortalWorkspace(options) {
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
const hasSourceChange = hasSourceConfigurationChange(options);
|
|
44
|
+
const hasPathChange = hasPathConfigurationChange(options);
|
|
45
|
+
if (!hasSourceChange && !hasPathChange) {
|
|
46
|
+
throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --path, --source-storage, or a --git-* flag.'));
|
|
65
47
|
}
|
|
66
48
|
const portal = validatePortalSlug(options.portal);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
const portalDir = hasPathChange
|
|
50
|
+
? resolvePortalSourcePath(portal, options.sourcePath)
|
|
51
|
+
: resolveSavedPortalSourcePath(options.env, portal) ?? '';
|
|
52
|
+
const appContext = await resolvePortalAppContext(options);
|
|
53
|
+
const { app } = appContext;
|
|
54
|
+
let config;
|
|
55
|
+
let remoteSynced = false;
|
|
74
56
|
const list = await listPortalWorkspaces({
|
|
75
57
|
env: options.env,
|
|
76
58
|
envName: options.envName,
|
|
77
59
|
cliVersion: options.cliVersion,
|
|
78
60
|
apiRequest: options.apiRequest,
|
|
61
|
+
appContext,
|
|
79
62
|
});
|
|
80
63
|
const remoteItem = findPortalListItem(list.items, portal);
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
if (!remoteItem) {
|
|
65
|
+
throw new Error(portalConfigureText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
|
|
66
|
+
}
|
|
67
|
+
if (!hasSourceChange) {
|
|
68
|
+
return {
|
|
69
|
+
app,
|
|
83
70
|
portal,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
71
|
+
portalDir,
|
|
72
|
+
config: undefined,
|
|
73
|
+
remoteSynced: false,
|
|
74
|
+
pathUpdated: hasPathChange,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const existingConfig = buildConfigFromRemoteOptions({
|
|
78
|
+
portal,
|
|
79
|
+
options: remoteItem.options,
|
|
80
|
+
sourceStorage: remoteItem.sourceStorage,
|
|
81
|
+
gitRepo: remoteItem.gitRepo,
|
|
82
|
+
gitBranch: remoteItem.gitBranch,
|
|
83
|
+
gitPath: remoteItem.gitPath,
|
|
84
|
+
});
|
|
85
|
+
config = buildPortalConfig({
|
|
91
86
|
portal,
|
|
92
87
|
sourceStorage: options.sourceStorage,
|
|
93
88
|
gitRepo: options.gitRepo,
|
|
@@ -95,23 +90,21 @@ export async function configurePortalWorkspace(options) {
|
|
|
95
90
|
gitPath: options.gitPath,
|
|
96
91
|
existingConfig,
|
|
97
92
|
});
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
apiRequest: options.apiRequest,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
93
|
+
await syncPortalConfigToRemote({
|
|
94
|
+
portal,
|
|
95
|
+
config,
|
|
96
|
+
currentOptions: remoteItem.options,
|
|
97
|
+
envName: options.envName,
|
|
98
|
+
cliVersion: options.cliVersion,
|
|
99
|
+
apiRequest: options.apiRequest,
|
|
100
|
+
});
|
|
101
|
+
remoteSynced = true;
|
|
110
102
|
return {
|
|
111
103
|
app,
|
|
112
104
|
portal,
|
|
113
105
|
portalDir,
|
|
114
106
|
config,
|
|
115
|
-
remoteSynced
|
|
107
|
+
remoteSynced,
|
|
108
|
+
pathUpdated: hasPathChange,
|
|
116
109
|
};
|
|
117
110
|
}
|
|
@@ -17,11 +17,14 @@ import { createGunzip } from 'node:zlib';
|
|
|
17
17
|
import * as tar from 'tar';
|
|
18
18
|
import { appendAppPublicPath, resolveAppPublicPath } from './app-public-path.js';
|
|
19
19
|
import { executeApiRequest } from './api-client.js';
|
|
20
|
+
import { resolveEnvPortalPath } from './auth-store.js';
|
|
20
21
|
import { resolveEnvRelativePath } from './cli-home.js';
|
|
21
22
|
import { translateCli } from './cli-locale.js';
|
|
23
|
+
import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
|
|
22
24
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
+
import { canReplacePortalDirectory } from './portal-path-safety.js';
|
|
26
|
+
import { buildPortalConfig, mergePortalConfigIntoOptions, } from './portal-config.js';
|
|
27
|
+
import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
|
|
25
28
|
const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
|
|
26
29
|
const DEFAULT_PORTAL_APP_NAME = 'main';
|
|
27
30
|
const TEMPLATE_COPY_EXCLUDED_NAMES = new Set(['.git', 'node_modules', '.DS_Store']);
|
|
@@ -42,6 +45,11 @@ const FIRST_OR_CREATE_PORTAL_OPERATION = {
|
|
|
42
45
|
},
|
|
43
46
|
],
|
|
44
47
|
};
|
|
48
|
+
const APP_INFO_OPERATION = {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
pathTemplate: '/app:getInfo',
|
|
51
|
+
parameters: [],
|
|
52
|
+
};
|
|
45
53
|
function trimValue(value) {
|
|
46
54
|
return String(value ?? '').trim();
|
|
47
55
|
}
|
|
@@ -319,6 +327,54 @@ export function resolvePortalAppFromApiBaseUrl(apiBaseUrl, appPublicPath) {
|
|
|
319
327
|
appPublicPath: resolveAppPublicPath(configuredPublicPath || inferredPublicPath),
|
|
320
328
|
};
|
|
321
329
|
}
|
|
330
|
+
function readAppNameFromInfo(data) {
|
|
331
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
const direct = data.name;
|
|
335
|
+
if (typeof direct === 'string' && direct.trim()) {
|
|
336
|
+
return direct.trim();
|
|
337
|
+
}
|
|
338
|
+
return readAppNameFromInfo(data.data);
|
|
339
|
+
}
|
|
340
|
+
function isValidPortalAppName(value) {
|
|
341
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value);
|
|
342
|
+
}
|
|
343
|
+
async function resolvePortalAppFromServer(options) {
|
|
344
|
+
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
345
|
+
try {
|
|
346
|
+
const response = await apiRequest({
|
|
347
|
+
cliVersion: options.cliVersion ?? '',
|
|
348
|
+
envName: options.envName,
|
|
349
|
+
flags: {},
|
|
350
|
+
operation: APP_INFO_OPERATION,
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const appName = readAppNameFromInfo(response.data);
|
|
356
|
+
return appName && isValidPortalAppName(appName) ? appName : undefined;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export async function resolvePortalAppContext(options) {
|
|
363
|
+
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
364
|
+
const resolvedApp = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
|
|
365
|
+
if (options.env.kind !== 'http') {
|
|
366
|
+
return {
|
|
367
|
+
...resolvedApp,
|
|
368
|
+
portalBaseApp: resolvedApp.app,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const serverApp = await resolvePortalAppFromServer(options);
|
|
372
|
+
return {
|
|
373
|
+
app: serverApp ?? resolvedApp.app,
|
|
374
|
+
appPublicPath: resolvedApp.appPublicPath,
|
|
375
|
+
portalBaseApp: resolvedApp.app,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
322
378
|
export function buildPortalBasePath(params) {
|
|
323
379
|
const segment = params.app === DEFAULT_PORTAL_APP_NAME
|
|
324
380
|
? `x/${params.portal}`
|
|
@@ -350,6 +406,19 @@ export function resolvePortalStoragePath(env) {
|
|
|
350
406
|
}
|
|
351
407
|
return path.resolve(process.cwd(), 'storage');
|
|
352
408
|
}
|
|
409
|
+
function resolveAbsolutePath(value) {
|
|
410
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
411
|
+
}
|
|
412
|
+
export function resolvePortalSourcePath(portal, sourcePath) {
|
|
413
|
+
return resolveAbsolutePath(trimValue(sourcePath) || portal);
|
|
414
|
+
}
|
|
415
|
+
export function resolveSavedPortalSourcePath(env, portal) {
|
|
416
|
+
const sourcePath = resolveEnvPortalPath(env.config, portal);
|
|
417
|
+
return sourcePath ? resolveAbsolutePath(sourcePath) : undefined;
|
|
418
|
+
}
|
|
419
|
+
export function resolvePortalDeployPath(params) {
|
|
420
|
+
return path.join(params.storagePath, 'portals', params.app, params.portal);
|
|
421
|
+
}
|
|
353
422
|
export async function createPortalWorkspace(options) {
|
|
354
423
|
const portal = validatePortalSlug(options.portal);
|
|
355
424
|
const title = trimValue(options.title) || titleFromPortalSlug(portal);
|
|
@@ -362,16 +431,18 @@ export async function createPortalWorkspace(options) {
|
|
|
362
431
|
});
|
|
363
432
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
364
433
|
const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
|
|
365
|
-
const
|
|
366
|
-
const { app
|
|
367
|
-
const
|
|
368
|
-
const portalParentDir = path.
|
|
369
|
-
const portalDir = path.join(portalParentDir, portal);
|
|
434
|
+
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
435
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
436
|
+
const portalDir = resolvePortalSourcePath(portal, options.sourcePath);
|
|
437
|
+
const portalParentDir = path.dirname(portalDir);
|
|
370
438
|
assertPortalDirIsInsideParent(portalParentDir, portalDir);
|
|
371
439
|
const targetExists = await pathExists(portalDir);
|
|
372
440
|
if (targetExists && !options.force) {
|
|
373
441
|
throw new Error(portalCreateText('errors.workspaceExists', { portalDir }, `Portal already exists: ${portalDir}\nPass --force to delete it and create a new portal.`));
|
|
374
442
|
}
|
|
443
|
+
if (targetExists && options.force && !(await canReplacePortalDirectory(portalDir))) {
|
|
444
|
+
throw new Error(portalCreateText('errors.workspaceNotReplaceable', { portalDir }, `Refusing to replace a non-portal directory: ${portalDir}`));
|
|
445
|
+
}
|
|
375
446
|
const template = await resolvePortalTemplate(options.template, {
|
|
376
447
|
npmRegistry: trimValue(options.env.config.npmRegistry),
|
|
377
448
|
runCommand: options.runCommand,
|
|
@@ -381,23 +452,32 @@ export async function createPortalWorkspace(options) {
|
|
|
381
452
|
let shouldCleanupTempDir = true;
|
|
382
453
|
try {
|
|
383
454
|
await copyTemplate(template.dir, tempDir);
|
|
455
|
+
await ensurePortalBuildHtmlReadsEnvOnly(tempDir);
|
|
384
456
|
await writeFile(path.join(tempDir, '.env'), [`NOCOBASE_API_URL=${envApiUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
|
|
385
457
|
await writeFile(path.join(tempDir, '.env.local'), [`NOCOBASE_API_URL=${apiBaseUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
|
|
386
|
-
await writePortalConfig(tempDir, portalConfig);
|
|
387
458
|
if (targetExists) {
|
|
388
459
|
await rm(portalDir, { recursive: true, force: true });
|
|
389
460
|
}
|
|
390
461
|
await rename(tempDir, portalDir);
|
|
391
462
|
shouldCleanupTempDir = false;
|
|
392
463
|
const hasPackageJson = await pathExists(path.join(portalDir, 'package.json'));
|
|
464
|
+
let dependenciesInstalled = false;
|
|
465
|
+
let installFailed = false;
|
|
393
466
|
if (hasPackageJson) {
|
|
394
467
|
const runCommand = options.runCommand ?? run;
|
|
395
|
-
await
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
468
|
+
const installCommand = await resolvePnpmInstallCommand(portalDir);
|
|
469
|
+
try {
|
|
470
|
+
await runPnpmInstallCommand(runCommand, installCommand.args, {
|
|
471
|
+
cwd: portalDir,
|
|
472
|
+
env: buildPortalCommandEnv(),
|
|
473
|
+
envMode: 'replace',
|
|
474
|
+
errorName: installCommand.errorName,
|
|
475
|
+
});
|
|
476
|
+
dependenciesInstalled = true;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
installFailed = true;
|
|
480
|
+
}
|
|
401
481
|
}
|
|
402
482
|
else {
|
|
403
483
|
options.onSkipInstall?.(portalCreateText('messages.skipInstall', { portalDir }, `Skipped pnpm install because package.json was not found in ${portalDir}.`));
|
|
@@ -421,6 +501,8 @@ export async function createPortalWorkspace(options) {
|
|
|
421
501
|
portalBase,
|
|
422
502
|
template,
|
|
423
503
|
installSkipped: !hasPackageJson,
|
|
504
|
+
dependenciesInstalled,
|
|
505
|
+
installFailed,
|
|
424
506
|
sourceStorage: portalConfig.sourceStorage,
|
|
425
507
|
};
|
|
426
508
|
}
|
|
@@ -12,11 +12,11 @@ import path from 'node:path';
|
|
|
12
12
|
import * as tar from 'tar';
|
|
13
13
|
import { executeApiRequest } from './api-client.js';
|
|
14
14
|
import { translateCli } from './cli-locale.js';
|
|
15
|
-
import {
|
|
15
|
+
import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
|
|
16
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalEnvApiUrl, resolveSavedPortalSourcePath, resolvePortalSourcePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
|
|
16
17
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
17
18
|
import { updatePortalEnvFiles } from './portal-env-files.js';
|
|
18
|
-
import {
|
|
19
|
-
import { run } from './run-npm.js';
|
|
19
|
+
import { resolvePnpmInstallCommand, run, runPnpmCommand, runPnpmInstallCommand } from './run-npm.js';
|
|
20
20
|
const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
|
|
21
21
|
const DEPLOY_OPERATION = {
|
|
22
22
|
method: 'POST',
|
|
@@ -106,12 +106,6 @@ async function chmodPortalDistTree(targetDir) {
|
|
|
106
106
|
}
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
-
async function ensurePortalDistPublicReadable(params) {
|
|
110
|
-
await chmod(path.join(params.storagePath, 'portals'), PORTAL_PUBLIC_DIR_MODE);
|
|
111
|
-
await chmod(path.join(params.storagePath, 'portals', params.app), PORTAL_PUBLIC_DIR_MODE);
|
|
112
|
-
await chmod(params.portalDir, PORTAL_PUBLIC_DIR_MODE);
|
|
113
|
-
await chmodPortalDistTree(params.distDir);
|
|
114
|
-
}
|
|
115
109
|
async function assertFileExists(filePath, message) {
|
|
116
110
|
try {
|
|
117
111
|
const fileStat = await stat(filePath);
|
|
@@ -173,9 +167,6 @@ async function syncMultiPortalRecord(params) {
|
|
|
173
167
|
uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
|
|
174
168
|
skipCreatePortalDirectory: true,
|
|
175
169
|
};
|
|
176
|
-
if (params.config) {
|
|
177
|
-
body.options = mergePortalConfigIntoOptions(params.config);
|
|
178
|
-
}
|
|
179
170
|
const response = await apiRequest({
|
|
180
171
|
cliVersion: params.cliVersion ?? '',
|
|
181
172
|
envName: params.envName,
|
|
@@ -192,23 +183,31 @@ async function syncMultiPortalRecord(params) {
|
|
|
192
183
|
export async function deployPortalWorkspace(options) {
|
|
193
184
|
const portal = validatePortalSlug(options.portal);
|
|
194
185
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
195
|
-
const
|
|
196
|
-
const { app, appPublicPath } =
|
|
197
|
-
const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
|
|
198
|
-
const
|
|
186
|
+
const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
|
|
187
|
+
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
188
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
189
|
+
const deployBase = buildPortalBasePath({ app, appPublicPath, portal });
|
|
190
|
+
const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
|
|
199
191
|
const distDir = path.join(portalDir, 'dist');
|
|
200
192
|
if (!(await pathExists(portalDir))) {
|
|
201
193
|
throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
|
|
202
194
|
}
|
|
203
195
|
await assertFileExists(path.join(portalDir, 'package.json'), portalDeployText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
|
|
204
|
-
const portalConfig = await readPortalConfig(portalDir);
|
|
205
196
|
await updatePortalEnvFiles({
|
|
206
197
|
portalDir,
|
|
207
198
|
apiBaseUrl,
|
|
208
199
|
portalBase,
|
|
209
200
|
});
|
|
201
|
+
await ensurePortalBuildHtmlReadsEnvOnly(portalDir);
|
|
210
202
|
const runCommand = options.runCommand ?? run;
|
|
211
|
-
await
|
|
203
|
+
const installCommand = await resolvePnpmInstallCommand(portalDir);
|
|
204
|
+
await runPnpmInstallCommand(runCommand, installCommand.args, {
|
|
205
|
+
cwd: portalDir,
|
|
206
|
+
env: buildPortalCommandEnv(),
|
|
207
|
+
envMode: 'replace',
|
|
208
|
+
errorName: installCommand.errorName,
|
|
209
|
+
});
|
|
210
|
+
await runPnpmCommand(runCommand, ['build'], {
|
|
212
211
|
cwd: portalDir,
|
|
213
212
|
env: buildPortalCommandEnv({
|
|
214
213
|
NOCOBASE_API_URL: apiBaseUrl,
|
|
@@ -217,33 +216,18 @@ export async function deployPortalWorkspace(options) {
|
|
|
217
216
|
envMode: 'replace',
|
|
218
217
|
errorName: 'pnpm build',
|
|
219
218
|
});
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
await runPnpmCommand(runCommand, ['build:html'], {
|
|
220
|
+
cwd: portalDir,
|
|
221
|
+
env: buildPortalCommandEnv({
|
|
222
|
+
NOCOBASE_API_URL: envApiUrl,
|
|
223
|
+
NOCOBASE_PORTAL_BASE: portalBase,
|
|
224
|
+
}),
|
|
225
|
+
envMode: 'replace',
|
|
226
|
+
errorName: 'pnpm build:html',
|
|
226
227
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
config: portalConfig,
|
|
231
|
-
envName: options.envName,
|
|
232
|
-
cliVersion: options.cliVersion,
|
|
233
|
-
apiRequest: options.apiRequest,
|
|
234
|
-
});
|
|
235
|
-
return {
|
|
236
|
-
app,
|
|
237
|
-
portal,
|
|
238
|
-
portalDir,
|
|
239
|
-
portalBase,
|
|
240
|
-
distDir,
|
|
241
|
-
mode: options.env.kind,
|
|
242
|
-
uploaded: false,
|
|
243
|
-
recordSynced: true,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
if (options.env.kind !== 'http') {
|
|
228
|
+
await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
|
|
229
|
+
await chmodPortalDistTree(distDir);
|
|
230
|
+
if (options.env.kind !== 'local' && options.env.kind !== 'docker' && options.env.kind !== 'http') {
|
|
247
231
|
throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
|
|
248
232
|
}
|
|
249
233
|
const archive = await packPortalDist(distDir);
|
|
@@ -253,7 +237,7 @@ export async function deployPortalWorkspace(options) {
|
|
|
253
237
|
archivePath: archive.archivePath,
|
|
254
238
|
app,
|
|
255
239
|
portal,
|
|
256
|
-
portalBase,
|
|
240
|
+
portalBase: deployBase,
|
|
257
241
|
envName: options.envName,
|
|
258
242
|
cliVersion: options.cliVersion,
|
|
259
243
|
apiRequest: options.apiRequest,
|
|
@@ -264,7 +248,6 @@ export async function deployPortalWorkspace(options) {
|
|
|
264
248
|
}
|
|
265
249
|
await syncMultiPortalRecord({
|
|
266
250
|
portal,
|
|
267
|
-
config: portalConfig,
|
|
268
251
|
envName: options.envName,
|
|
269
252
|
cliVersion: options.cliVersion,
|
|
270
253
|
apiRequest: options.apiRequest,
|
|
@@ -276,7 +259,7 @@ export async function deployPortalWorkspace(options) {
|
|
|
276
259
|
portalBase,
|
|
277
260
|
distDir,
|
|
278
261
|
serverDistPath: uploadResult.distPath,
|
|
279
|
-
mode:
|
|
262
|
+
mode: options.env.kind,
|
|
280
263
|
uploaded: true,
|
|
281
264
|
recordSynced: true,
|
|
282
265
|
};
|
|
@@ -10,23 +10,22 @@ import { rm, stat } from 'node:fs/promises';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { executeApiRequest } from './api-client.js';
|
|
12
12
|
import { translateCli } from './cli-locale.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isUnsafePortalDeletePath } from './portal-path-safety.js';
|
|
14
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalDeployPath, resolveSavedPortalSourcePath, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
|
|
14
15
|
const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
|
|
15
16
|
const DESTROY_PORTAL_OPERATION = {
|
|
16
17
|
method: 'POST',
|
|
17
18
|
pathTemplate: '/multiPortals:destroy',
|
|
18
19
|
parameters: [
|
|
19
20
|
{
|
|
20
|
-
name: '
|
|
21
|
-
flagName: '
|
|
21
|
+
name: 'filter',
|
|
22
|
+
flagName: 'filter',
|
|
22
23
|
in: 'query',
|
|
24
|
+
type: 'object',
|
|
23
25
|
required: true,
|
|
24
26
|
},
|
|
25
27
|
],
|
|
26
28
|
};
|
|
27
|
-
function trimValue(value) {
|
|
28
|
-
return String(value ?? '').trim();
|
|
29
|
-
}
|
|
30
29
|
async function pathExists(target) {
|
|
31
30
|
try {
|
|
32
31
|
await stat(target);
|
|
@@ -48,7 +47,9 @@ async function destroyMultiPortalRecord(params) {
|
|
|
48
47
|
cliVersion: params.cliVersion ?? '',
|
|
49
48
|
envName: params.envName,
|
|
50
49
|
flags: {
|
|
51
|
-
|
|
50
|
+
filter: {
|
|
51
|
+
portalName: params.portal,
|
|
52
|
+
},
|
|
52
53
|
},
|
|
53
54
|
operation: DESTROY_PORTAL_OPERATION,
|
|
54
55
|
});
|
|
@@ -62,21 +63,29 @@ async function destroyMultiPortalRecord(params) {
|
|
|
62
63
|
}
|
|
63
64
|
export async function destroyPortalWorkspace(options) {
|
|
64
65
|
const portal = validatePortalSlug(options.portal);
|
|
65
|
-
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
66
66
|
const storagePath = resolvePortalStoragePath(options.env);
|
|
67
|
-
const { app, appPublicPath } =
|
|
68
|
-
const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
|
|
69
|
-
const
|
|
70
|
-
const
|
|
67
|
+
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
68
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
69
|
+
const portalDeployDir = resolvePortalDeployPath({ storagePath, app, portal });
|
|
70
|
+
const portalDevDir = resolveSavedPortalSourcePath(options.env, portal) ?? '';
|
|
71
71
|
const mode = options.env.kind;
|
|
72
72
|
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
73
73
|
throw new Error(portalDestroyText('errors.unsupportedEnvKind', { kind: mode }, `Cannot destroy a portal for ${mode} envs in the first version.`));
|
|
74
74
|
}
|
|
75
75
|
const destroyMode = mode;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
if (portalDevDir) {
|
|
77
|
+
assertPortalDirIsInsideParent(path.dirname(portalDevDir), portalDevDir);
|
|
78
|
+
}
|
|
79
|
+
assertPortalDirIsInsideParent(path.dirname(portalDeployDir), portalDeployDir);
|
|
80
|
+
const developmentPathIsDeploymentPath = portalDevDir
|
|
81
|
+
? path.resolve(portalDevDir) === path.resolve(portalDeployDir)
|
|
82
|
+
: false;
|
|
83
|
+
const deploymentPathExists = await pathExists(portalDeployDir);
|
|
84
|
+
const developmentPathExists = options.deleteDevPath && portalDevDir && !developmentPathIsDeploymentPath
|
|
85
|
+
? await pathExists(portalDevDir)
|
|
86
|
+
: false;
|
|
87
|
+
if (developmentPathExists && (await isUnsafePortalDeletePath(portalDevDir))) {
|
|
88
|
+
throw new Error(portalDestroyText('errors.unsafeDevelopmentPath', { portalDir: portalDevDir }, `Refusing to delete an unsafe portal development path: ${portalDevDir}`));
|
|
80
89
|
}
|
|
81
90
|
const recordDeleted = await destroyMultiPortalRecord({
|
|
82
91
|
portal,
|
|
@@ -85,16 +94,21 @@ export async function destroyPortalWorkspace(options) {
|
|
|
85
94
|
force: options.force,
|
|
86
95
|
apiRequest: options.apiRequest,
|
|
87
96
|
});
|
|
88
|
-
if (
|
|
89
|
-
await rm(
|
|
97
|
+
if (deploymentPathExists) {
|
|
98
|
+
await rm(portalDeployDir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
if (developmentPathExists) {
|
|
101
|
+
await rm(portalDevDir, { recursive: true, force: true });
|
|
90
102
|
}
|
|
91
103
|
return {
|
|
92
104
|
app,
|
|
93
105
|
portal,
|
|
94
|
-
|
|
106
|
+
developmentPath: portalDevDir,
|
|
107
|
+
deploymentPath: portalDeployDir,
|
|
95
108
|
portalBase,
|
|
96
109
|
mode: destroyMode,
|
|
97
110
|
recordDeleted,
|
|
98
|
-
|
|
111
|
+
developmentPathDeleted: developmentPathExists || (developmentPathIsDeploymentPath && deploymentPathExists),
|
|
112
|
+
deploymentPathDeleted: deploymentPathExists,
|
|
99
113
|
};
|
|
100
114
|
}
|
package/dist/lib/portal-dev.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
import { stat } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { translateCli } from './cli-locale.js';
|
|
12
|
-
import { buildPortalBasePath,
|
|
12
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolveSavedPortalSourcePath, resolvePortalSourcePath, validatePortalSlug, } from './portal-create.js';
|
|
13
13
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
14
14
|
import { updatePortalEnvFiles } from './portal-env-files.js';
|
|
15
|
-
import { run } from './run-npm.js';
|
|
15
|
+
import { run, runPnpmCommand } from './run-npm.js';
|
|
16
16
|
const portalDevText = (key, values, fallback) => translateCli(`commands.portalDev.${key}`, values, { fallback });
|
|
17
17
|
function trimValue(value) {
|
|
18
18
|
return String(value ?? '').trim();
|
|
@@ -44,10 +44,9 @@ export async function devPortalWorkspace(options) {
|
|
|
44
44
|
if (options.env.kind === 'ssh') {
|
|
45
45
|
throw new Error(portalDevText('errors.sshUnsupported', undefined, 'Cannot start a portal in dev mode for ssh envs in the first version.'));
|
|
46
46
|
}
|
|
47
|
-
const
|
|
48
|
-
const { app
|
|
49
|
-
const
|
|
50
|
-
const portalDir = path.join(storagePath, 'portals', app, portal);
|
|
47
|
+
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
48
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
49
|
+
const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
|
|
51
50
|
if (!(await pathExists(portalDir))) {
|
|
52
51
|
throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
|
|
53
52
|
}
|
|
@@ -66,7 +65,7 @@ export async function devPortalWorkspace(options) {
|
|
|
66
65
|
};
|
|
67
66
|
options.onStart?.(result);
|
|
68
67
|
const runCommand = options.runCommand ?? run;
|
|
69
|
-
await runCommand
|
|
68
|
+
await runPnpmCommand(runCommand, ['dev'], {
|
|
70
69
|
cwd: portalDir,
|
|
71
70
|
env: buildPortalCommandEnv({
|
|
72
71
|
NOCOBASE_API_URL: apiBaseUrl,
|
|
@@ -42,8 +42,9 @@ export async function upsertPortalEnvFile(filePath, values) {
|
|
|
42
42
|
await writeFile(filePath, upsertEnvContent(content, values), 'utf-8');
|
|
43
43
|
}
|
|
44
44
|
export async function updatePortalEnvFiles(params) {
|
|
45
|
+
const envApiUrl = resolvePortalEnvApiUrl(params.apiBaseUrl);
|
|
45
46
|
await upsertPortalEnvFile(path.join(params.portalDir, '.env'), {
|
|
46
|
-
NOCOBASE_API_URL:
|
|
47
|
+
NOCOBASE_API_URL: envApiUrl,
|
|
47
48
|
NOCOBASE_PORTAL_BASE: params.portalBase,
|
|
48
49
|
});
|
|
49
50
|
await upsertPortalEnvFile(path.join(params.portalDir, '.env.local'), {
|
package/dist/lib/portal-info.js
CHANGED
|
@@ -10,9 +10,6 @@ import { translateCli } from './cli-locale.js';
|
|
|
10
10
|
import { toPortalOutputItem } from './portal-list.js';
|
|
11
11
|
const portalInfoText = (key, values, fallback) => translateCli(`commands.portalInfo.${key}`, values, { fallback });
|
|
12
12
|
function formatBoolean(value) {
|
|
13
|
-
if (value === null) {
|
|
14
|
-
return '';
|
|
15
|
-
}
|
|
16
13
|
return value ? 'yes' : 'no';
|
|
17
14
|
}
|
|
18
15
|
export function findPortalListItem(items, portal) {
|
|
@@ -24,8 +21,8 @@ export function formatPortalInfo(item) {
|
|
|
24
21
|
`${portalInfoText('fields.name', undefined, 'Name')}: ${outputItem.name}`,
|
|
25
22
|
`${portalInfoText('fields.url', undefined, 'URL')}: ${outputItem.url}`,
|
|
26
23
|
`${portalInfoText('fields.portalType', undefined, 'Portal type')}: ${outputItem.portalType}`,
|
|
27
|
-
`${portalInfoText('fields.
|
|
24
|
+
`${portalInfoText('fields.developmentPath', undefined, 'Development path')}: ${outputItem.developmentPath}`,
|
|
25
|
+
`${portalInfoText('fields.deploymentPath', undefined, 'Deployment path')}: ${outputItem.deploymentPath}`,
|
|
28
26
|
`${portalInfoText('fields.enabled', undefined, 'Enabled')}: ${formatBoolean(outputItem.enabled)}`,
|
|
29
|
-
`${portalInfoText('fields.localSynced', undefined, 'Local synced')}: ${formatBoolean(outputItem.localSynced)}`,
|
|
30
27
|
].join('\n');
|
|
31
28
|
}
|