@nocobase/cli 3.0.0-alpha.4 → 3.0.0-alpha.6
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/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/auth-store.js +51 -1
- package/dist/lib/env-auth.js +1 -1
- package/dist/lib/env-config.js +5 -0
- package/dist/lib/env-portal-config.js +30 -0
- 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 +17 -49
- package/dist/lib/portal-destroy.js +34 -20
- package/dist/lib/portal-dev.js +4 -5
- 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 +186 -54
- package/dist/lib/run-npm.js +46 -0
- package/dist/locale/en-US.json +45 -14
- package/dist/locale/zh-CN.json +45 -14
- package/package.json +2 -2
|
@@ -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
|
}),
|
|
@@ -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, runPnpmCommand } 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,27 +183,29 @@ 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, {
|
|
212
205
|
cwd: portalDir,
|
|
213
206
|
env: buildPortalCommandEnv(),
|
|
214
207
|
envMode: 'replace',
|
|
215
|
-
errorName:
|
|
208
|
+
errorName: installCommand.errorName,
|
|
216
209
|
});
|
|
217
210
|
await runPnpmCommand(runCommand, ['build'], {
|
|
218
211
|
cwd: portalDir,
|
|
@@ -226,39 +219,15 @@ export async function deployPortalWorkspace(options) {
|
|
|
226
219
|
await runPnpmCommand(runCommand, ['build:html'], {
|
|
227
220
|
cwd: portalDir,
|
|
228
221
|
env: buildPortalCommandEnv({
|
|
229
|
-
NOCOBASE_API_URL:
|
|
222
|
+
NOCOBASE_API_URL: envApiUrl,
|
|
230
223
|
NOCOBASE_PORTAL_BASE: portalBase,
|
|
231
224
|
}),
|
|
232
225
|
envMode: 'replace',
|
|
233
226
|
errorName: 'pnpm build:html',
|
|
234
227
|
});
|
|
235
228
|
await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
|
|
236
|
-
await
|
|
237
|
-
|
|
238
|
-
app,
|
|
239
|
-
portalDir,
|
|
240
|
-
distDir,
|
|
241
|
-
});
|
|
242
|
-
if (options.env.kind === 'local' || options.env.kind === 'docker') {
|
|
243
|
-
await syncMultiPortalRecord({
|
|
244
|
-
portal,
|
|
245
|
-
config: portalConfig,
|
|
246
|
-
envName: options.envName,
|
|
247
|
-
cliVersion: options.cliVersion,
|
|
248
|
-
apiRequest: options.apiRequest,
|
|
249
|
-
});
|
|
250
|
-
return {
|
|
251
|
-
app,
|
|
252
|
-
portal,
|
|
253
|
-
portalDir,
|
|
254
|
-
portalBase,
|
|
255
|
-
distDir,
|
|
256
|
-
mode: options.env.kind,
|
|
257
|
-
uploaded: false,
|
|
258
|
-
recordSynced: true,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (options.env.kind !== 'http') {
|
|
229
|
+
await chmodPortalDistTree(distDir);
|
|
230
|
+
if (options.env.kind !== 'local' && options.env.kind !== 'docker' && options.env.kind !== 'http') {
|
|
262
231
|
throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
|
|
263
232
|
}
|
|
264
233
|
const archive = await packPortalDist(distDir);
|
|
@@ -268,7 +237,7 @@ export async function deployPortalWorkspace(options) {
|
|
|
268
237
|
archivePath: archive.archivePath,
|
|
269
238
|
app,
|
|
270
239
|
portal,
|
|
271
|
-
portalBase,
|
|
240
|
+
portalBase: deployBase,
|
|
272
241
|
envName: options.envName,
|
|
273
242
|
cliVersion: options.cliVersion,
|
|
274
243
|
apiRequest: options.apiRequest,
|
|
@@ -279,7 +248,6 @@ export async function deployPortalWorkspace(options) {
|
|
|
279
248
|
}
|
|
280
249
|
await syncMultiPortalRecord({
|
|
281
250
|
portal,
|
|
282
|
-
config: portalConfig,
|
|
283
251
|
envName: options.envName,
|
|
284
252
|
cliVersion: options.cliVersion,
|
|
285
253
|
apiRequest: options.apiRequest,
|
|
@@ -291,7 +259,7 @@ export async function deployPortalWorkspace(options) {
|
|
|
291
259
|
portalBase,
|
|
292
260
|
distDir,
|
|
293
261
|
serverDistPath: uploadResult.distPath,
|
|
294
|
-
mode:
|
|
262
|
+
mode: options.env.kind,
|
|
295
263
|
uploaded: true,
|
|
296
264
|
recordSynced: true,
|
|
297
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,7 +9,7 @@
|
|
|
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
15
|
import { run, runPnpmCommand } from './run-npm.js';
|
|
@@ -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
|
}
|
|
@@ -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'), {
|