@nocobase/cli 2.2.0-beta.5 → 2.2.0-beta.7
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/app/restart.js +38 -0
- package/dist/commands/app/start.js +92 -0
- package/dist/commands/app/upgrade.js +11 -0
- package/dist/commands/init.js +15 -1
- package/dist/commands/install.js +135 -4
- package/dist/commands/source/dev.js +9 -5
- package/dist/commands/source/download.js +66 -0
- package/dist/lib/app-managed-resources.js +101 -1
- package/dist/lib/env-config.js +1 -0
- package/dist/lib/hook-script.js +160 -0
- package/dist/lib/source-publish.js +2 -2
- package/package.json +2 -2
|
@@ -14,6 +14,7 @@ import { recreateSavedDockerApp } from '../../lib/app-managed-resources.js';
|
|
|
14
14
|
import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
15
15
|
import { run } from '../../lib/run-npm.js';
|
|
16
16
|
import { announceTargetEnv, failTask, startTask, succeedTask } from '../../lib/ui.js';
|
|
17
|
+
import { buildHookContext, resolveHookScriptPath, runHookScriptHook } from '../../lib/hook-script.js';
|
|
17
18
|
function argvHasToken(argv, tokens) {
|
|
18
19
|
return tokens.some((token) => argv.includes(token));
|
|
19
20
|
}
|
|
@@ -62,6 +63,39 @@ function resolveDisplayAppUrl(apiBaseUrl, port, appPublicPath) {
|
|
|
62
63
|
}
|
|
63
64
|
return formatAppUrl(port, appPublicPath);
|
|
64
65
|
}
|
|
66
|
+
async function runDockerAfterAppStartHookIfNeeded(runtime) {
|
|
67
|
+
const hookScript = runtime.env.config?.hookScript;
|
|
68
|
+
if (!hookScript) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
72
|
+
appPath: runtime.env.appPath,
|
|
73
|
+
hookScript,
|
|
74
|
+
});
|
|
75
|
+
if (!hookScriptPath) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const context = buildHookContext({
|
|
79
|
+
phase: 'app-start',
|
|
80
|
+
command: 'app:restart',
|
|
81
|
+
envName: runtime.envName,
|
|
82
|
+
source: runtime.source,
|
|
83
|
+
version: runtime.env.config?.downloadVersion,
|
|
84
|
+
appPath: runtime.env.appPath,
|
|
85
|
+
sourcePath: runtime.env.sourcePath,
|
|
86
|
+
storagePath: runtime.env.storagePath,
|
|
87
|
+
hookScript: String(hookScript).trim(),
|
|
88
|
+
envConfig: runtime.env.config,
|
|
89
|
+
});
|
|
90
|
+
if (!context) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await runHookScriptHook({
|
|
94
|
+
hookScriptPath,
|
|
95
|
+
hookName: 'afterAppStart',
|
|
96
|
+
context,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
65
99
|
export default class AppRestart extends Command {
|
|
66
100
|
static hidden = false;
|
|
67
101
|
static description = 'Restart NocoBase for the selected env. When applicable, the CLI synchronizes licensed commercial plugins first, then restarts the local app or recreates the saved Docker container.';
|
|
@@ -199,6 +233,7 @@ export default class AppRestart extends Command {
|
|
|
199
233
|
logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
|
|
200
234
|
...(flags.verbose ? { verbose: true } : {}),
|
|
201
235
|
});
|
|
236
|
+
await runDockerAfterAppStartHookIfNeeded(runtime);
|
|
202
237
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
203
238
|
return;
|
|
204
239
|
}
|
|
@@ -222,6 +257,9 @@ export default class AppRestart extends Command {
|
|
|
222
257
|
if (daemonFlagWasProvided) {
|
|
223
258
|
startArgv.push(flags.daemon === false ? '--no-daemon' : '--daemon');
|
|
224
259
|
}
|
|
260
|
+
if (runtime.env.config?.hookScript) {
|
|
261
|
+
startArgv.push('--hook-command', 'app:restart');
|
|
262
|
+
}
|
|
225
263
|
await runWithSuppressedTargetEnvLog(async () => {
|
|
226
264
|
await runCommand('app:start', startArgv);
|
|
227
265
|
});
|
|
@@ -18,6 +18,7 @@ import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
|
18
18
|
import { readManagedRuntimeEnvValues } from '../../lib/managed-env-file.js';
|
|
19
19
|
import { run } from '../../lib/run-npm.js';
|
|
20
20
|
import { announceTargetEnv, failTask, printInfo, printWarning, startTask, succeedTask } from '../../lib/ui.js';
|
|
21
|
+
import { buildHookContext, resolveHookScriptPath, runHookScriptHook, } from '../../lib/hook-script.js';
|
|
21
22
|
function shouldPrintStartSuccess() {
|
|
22
23
|
return process.env.NB_SKIP_APP_START_SUCCESS_LOG !== '1';
|
|
23
24
|
}
|
|
@@ -34,6 +35,57 @@ function buildLicenseSyncArgv(envName, options) {
|
|
|
34
35
|
}
|
|
35
36
|
return argv;
|
|
36
37
|
}
|
|
38
|
+
function resolveHookCommand(value) {
|
|
39
|
+
const text = String(value ?? '').trim();
|
|
40
|
+
if (text === 'app:restart' || text === 'app:upgrade') {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
return 'app:start';
|
|
44
|
+
}
|
|
45
|
+
function resolveAppStartHookPhase(options) {
|
|
46
|
+
if (options.isPreparedEnv) {
|
|
47
|
+
return 'init';
|
|
48
|
+
}
|
|
49
|
+
if (options.command === 'app:upgrade') {
|
|
50
|
+
return 'upgrade';
|
|
51
|
+
}
|
|
52
|
+
return 'app-start';
|
|
53
|
+
}
|
|
54
|
+
async function runRuntimeHookIfNeeded(params) {
|
|
55
|
+
const hookScript = params.runtime.env.config?.hookScript;
|
|
56
|
+
if (!hookScript) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
60
|
+
appPath: params.runtime.env.appPath,
|
|
61
|
+
hookScript,
|
|
62
|
+
});
|
|
63
|
+
if (!hookScriptPath) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const context = buildHookContext({
|
|
67
|
+
phase: params.phase,
|
|
68
|
+
command: params.command,
|
|
69
|
+
envName: params.runtime.envName,
|
|
70
|
+
source: params.runtime.source,
|
|
71
|
+
version: params.runtime.env.config?.downloadVersion,
|
|
72
|
+
appPath: params.runtime.env.appPath,
|
|
73
|
+
sourcePath: params.runtime.env.sourcePath,
|
|
74
|
+
storagePath: params.runtime.env.storagePath,
|
|
75
|
+
hookScript: String(hookScript).trim(),
|
|
76
|
+
envConfig: params.hookName === 'afterAppStart' && params.phase === 'init'
|
|
77
|
+
? { ...params.runtime.env.config, setupState: 'installed' }
|
|
78
|
+
: params.runtime.env.config,
|
|
79
|
+
});
|
|
80
|
+
if (!context) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await runHookScriptHook({
|
|
84
|
+
hookScriptPath,
|
|
85
|
+
hookName: params.hookName,
|
|
86
|
+
context,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
37
89
|
async function runWithSuppressedTargetEnvLog(task) {
|
|
38
90
|
const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
|
|
39
91
|
process.env.NB_SKIP_TARGET_ENV_LOG = '1';
|
|
@@ -171,6 +223,10 @@ export default class AppStart extends Command {
|
|
|
171
223
|
description: 'Show raw startup output from the underlying local or Docker command',
|
|
172
224
|
default: false,
|
|
173
225
|
}),
|
|
226
|
+
'hook-command': Flags.string({
|
|
227
|
+
hidden: true,
|
|
228
|
+
options: ['app:start', 'app:restart', 'app:upgrade'],
|
|
229
|
+
}),
|
|
174
230
|
};
|
|
175
231
|
async run() {
|
|
176
232
|
const { flags } = await this.parse(AppStart);
|
|
@@ -192,6 +248,12 @@ export default class AppStart extends Command {
|
|
|
192
248
|
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
193
249
|
const preparedInitEnvVars = buildInitAppEnvVarsFromConfig(runtime?.env.config);
|
|
194
250
|
const isPreparedEnv = isPreparedSetupState(runtime?.env.config?.setupState);
|
|
251
|
+
const hookCommand = resolveHookCommand(flags['hook-command']);
|
|
252
|
+
const hookPhase = resolveAppStartHookPhase({
|
|
253
|
+
isPreparedEnv,
|
|
254
|
+
command: hookCommand,
|
|
255
|
+
});
|
|
256
|
+
const shouldRunBeforeAppInstall = isPreparedEnv || hookCommand === 'app:upgrade';
|
|
195
257
|
const runCommand = this.config.runCommand.bind(this.config);
|
|
196
258
|
const commandStdio = flags.verbose ? 'inherit' : 'ignore';
|
|
197
259
|
if (!runtime) {
|
|
@@ -249,6 +311,14 @@ export default class AppStart extends Command {
|
|
|
249
311
|
errorName: 'docker rm',
|
|
250
312
|
stdio: commandStdio,
|
|
251
313
|
}).catch(() => undefined);
|
|
314
|
+
if (shouldRunBeforeAppInstall) {
|
|
315
|
+
await runRuntimeHookIfNeeded({
|
|
316
|
+
hookName: 'beforeAppInstall',
|
|
317
|
+
runtime,
|
|
318
|
+
phase: hookPhase,
|
|
319
|
+
command: hookCommand,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
252
322
|
await recreateSavedDockerApp(runtime, {
|
|
253
323
|
initEnvVars: isPreparedEnv ? preparedInitEnvVars : undefined,
|
|
254
324
|
verbose: flags.verbose,
|
|
@@ -270,6 +340,12 @@ export default class AppStart extends Command {
|
|
|
270
340
|
if (isPreparedEnv) {
|
|
271
341
|
await finalizePreparedEnv(runtime.envName);
|
|
272
342
|
}
|
|
343
|
+
await runRuntimeHookIfNeeded({
|
|
344
|
+
hookName: 'afterAppStart',
|
|
345
|
+
runtime,
|
|
346
|
+
phase: hookPhase,
|
|
347
|
+
command: hookCommand,
|
|
348
|
+
});
|
|
273
349
|
if (shouldPrintStartSuccess()) {
|
|
274
350
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
275
351
|
}
|
|
@@ -285,6 +361,8 @@ export default class AppStart extends Command {
|
|
|
285
361
|
const downloadableRuntime = runtime;
|
|
286
362
|
await ensureSavedLocalSource(downloadableRuntime, runCommand, {
|
|
287
363
|
verbose: flags.verbose,
|
|
364
|
+
hookPhase: isPreparedEnv ? 'init' : 'restore',
|
|
365
|
+
hookCommand,
|
|
288
366
|
onStartTask: startTask,
|
|
289
367
|
onSucceedTask: succeedTask,
|
|
290
368
|
onFailTask: failTask,
|
|
@@ -381,6 +459,14 @@ export default class AppStart extends Command {
|
|
|
381
459
|
...managedAppLifecycleEnvVars(),
|
|
382
460
|
...(isPreparedEnv ? preparedInitEnvVars : {}),
|
|
383
461
|
};
|
|
462
|
+
if (shouldRunBeforeAppInstall) {
|
|
463
|
+
await runRuntimeHookIfNeeded({
|
|
464
|
+
hookName: 'beforeAppInstall',
|
|
465
|
+
runtime,
|
|
466
|
+
phase: hookPhase,
|
|
467
|
+
command: hookCommand,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
384
470
|
await runLocalNocoBaseCommand(runtime, npmArgs, {
|
|
385
471
|
env: startEnv,
|
|
386
472
|
stdio: commandStdio,
|
|
@@ -394,6 +480,12 @@ export default class AppStart extends Command {
|
|
|
394
480
|
if (isPreparedEnv) {
|
|
395
481
|
await finalizePreparedEnv(runtime.envName);
|
|
396
482
|
}
|
|
483
|
+
await runRuntimeHookIfNeeded({
|
|
484
|
+
hookName: 'afterAppStart',
|
|
485
|
+
runtime,
|
|
486
|
+
phase: hookPhase,
|
|
487
|
+
command: hookCommand,
|
|
488
|
+
});
|
|
397
489
|
if (shouldPrintStartSuccess()) {
|
|
398
490
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
399
491
|
}
|
|
@@ -12,6 +12,7 @@ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '..
|
|
|
12
12
|
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
13
13
|
import { DEFAULT_DOCKER_REGISTRY } from "../../lib/docker-image.js";
|
|
14
14
|
import { confirm } from "../../lib/inquirer.js";
|
|
15
|
+
import { resolveHookScriptPath } from '../../lib/hook-script.js';
|
|
15
16
|
import { announceTargetEnv, isInteractiveTerminal, printInfo, printWarning, succeedTask } from '../../lib/ui.js';
|
|
16
17
|
import { resolveAppUrlFromApiBaseUrl } from '../env/shared.js';
|
|
17
18
|
function trimValue(value) {
|
|
@@ -345,6 +346,13 @@ export default class AppUpgrade extends Command {
|
|
|
345
346
|
if (runtime.env.config.buildDts === true) {
|
|
346
347
|
argv.push('--build-dts');
|
|
347
348
|
}
|
|
349
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
350
|
+
appPath: runtime.env.appPath,
|
|
351
|
+
hookScript: runtime.env.config.hookScript,
|
|
352
|
+
});
|
|
353
|
+
if (hookScriptPath) {
|
|
354
|
+
argv.push('--hook-script', hookScriptPath, '--hook-phase', 'upgrade', '--hook-command', 'app:upgrade', '--hook-env-name', runtime.envName, '--hook-app-path', runtime.env.appPath, '--hook-storage-path', runtime.env.storagePath);
|
|
355
|
+
}
|
|
348
356
|
return argv;
|
|
349
357
|
}
|
|
350
358
|
static buildDockerDownloadArgv(runtime, downloadVersion, options) {
|
|
@@ -497,6 +505,9 @@ export default class AppUpgrade extends Command {
|
|
|
497
505
|
await runWithSuppressedTargetEnvLog(async () => {
|
|
498
506
|
const startArgv = buildManagedActionArgv(runtime.envName, parsed, { quickstart: true });
|
|
499
507
|
startArgv.push('--no-sync-licensed-plugins');
|
|
508
|
+
if (runtime.env.config.hookScript) {
|
|
509
|
+
startArgv.push('--hook-command', 'app:upgrade');
|
|
510
|
+
}
|
|
500
511
|
await runWithSuppressedStartSuccessLog(async () => {
|
|
501
512
|
await runCommand('app:start', startArgv);
|
|
502
513
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -15,7 +15,7 @@ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
|
|
|
15
15
|
import { getEnv, upsertEnv } from "../lib/auth-store.js";
|
|
16
16
|
import { runPromptCatalog, } from "../lib/prompt-catalog.js";
|
|
17
17
|
import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
|
|
18
|
-
import { resolveDefaultConfigScope } from '../lib/cli-home.js';
|
|
18
|
+
import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath } from '../lib/cli-home.js';
|
|
19
19
|
import { resolveDefaultApiHost, resolveDefaultUiHost } from '../lib/cli-config.js';
|
|
20
20
|
import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
|
|
21
21
|
import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
|
|
@@ -25,6 +25,7 @@ import { installNocoBaseSkills, isNpmRegistryUnavailable } from '../lib/skills-m
|
|
|
25
25
|
import { omitKeys, pickKeys } from "../lib/object-utils.js";
|
|
26
26
|
import { ENV_CONFIG_SCHEMA_VERSION } from '../lib/env-config.js';
|
|
27
27
|
import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
|
|
28
|
+
import { persistHookScript } from '../lib/hook-script.js';
|
|
28
29
|
import Download from "./download.js";
|
|
29
30
|
import EnvAdd from "./env/add.js";
|
|
30
31
|
import Install, { defaultDbPortForDialect } from "./install.js";
|
|
@@ -1029,6 +1030,14 @@ Prompt modes:
|
|
|
1029
1030
|
const skipDownload = results.skipDownload === true;
|
|
1030
1031
|
const appKey = resolveManagedAppKey(results.appKey ?? existingEnv?.config.appKey);
|
|
1031
1032
|
const timeZone = resolveManagedTimeZone(results.timeZone ?? existingEnv?.config.timezone);
|
|
1033
|
+
const hookScriptInput = String(flags['hook-script'] ?? '').trim();
|
|
1034
|
+
const hookAppPath = appPath || `./${envName}/`;
|
|
1035
|
+
const hookScript = hookScriptInput
|
|
1036
|
+
? await persistHookScript({
|
|
1037
|
+
sourcePath: hookScriptInput,
|
|
1038
|
+
appPath: resolveConfiguredEnvPath(hookAppPath) ?? resolveEnvRelativePath(hookAppPath),
|
|
1039
|
+
})
|
|
1040
|
+
: String(results.hookScript ?? '').trim();
|
|
1032
1041
|
const builtinDb = explicitDbHostFlag(flags)
|
|
1033
1042
|
? false
|
|
1034
1043
|
: results.builtinDb === undefined
|
|
@@ -1055,6 +1064,7 @@ Prompt modes:
|
|
|
1055
1064
|
...(dockerPlatform ? { dockerPlatform } : {}),
|
|
1056
1065
|
...(gitUrl ? { gitUrl } : {}),
|
|
1057
1066
|
...(npmRegistry ? { npmRegistry } : {}),
|
|
1067
|
+
...(hookScript ? { hookScript } : {}),
|
|
1058
1068
|
...(appPath ? { appPath } : {}),
|
|
1059
1069
|
...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
|
|
1060
1070
|
...(storagePath && !areConfiguredPathsEquivalent(storagePath, derivedStoragePath) ? { storagePath } : {}),
|
|
@@ -1130,6 +1140,10 @@ Prompt modes:
|
|
|
1130
1140
|
if (flags['prepare-only']) {
|
|
1131
1141
|
argv.push('--prepare-only');
|
|
1132
1142
|
}
|
|
1143
|
+
const hookScript = String(results.hookScript ?? flags['hook-script'] ?? '').trim();
|
|
1144
|
+
if (hookScript) {
|
|
1145
|
+
argv.push('--hook-script', hookScript);
|
|
1146
|
+
}
|
|
1133
1147
|
if (flags.verbose) {
|
|
1134
1148
|
argv.push('--verbose');
|
|
1135
1149
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -15,7 +15,7 @@ import { appendAppPublicPath } from '../lib/app-public-path.js';
|
|
|
15
15
|
import { runPromptCatalog, } from "../lib/prompt-catalog.js";
|
|
16
16
|
import { applyCliLocale, localeText, resolveCliLocale, translateCli } from "../lib/cli-locale.js";
|
|
17
17
|
import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
|
|
18
|
-
import { defaultDockerContainerPrefix, defaultDockerNetworkName } from '../lib/app-runtime.js';
|
|
18
|
+
import { defaultDockerContainerPrefix, defaultDockerNetworkName, managedAppLifecycleEnvVars, } from '../lib/app-runtime.js';
|
|
19
19
|
import { resolveDefaultApiHost, resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
|
|
20
20
|
import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../lib/docker-image.js";
|
|
21
21
|
import { findAvailableTcpPort, validateAppPublicPath, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
|
|
@@ -29,6 +29,7 @@ import { buildStoredEnvConfig } from '../lib/env-config.js';
|
|
|
29
29
|
import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
|
|
30
30
|
import { startDockerLogFollower } from '../lib/docker-log-stream.js';
|
|
31
31
|
import { buildInitAppEnvVarsFromConfig } from '../lib/managed-init-env.js';
|
|
32
|
+
import { buildHookContext, persistHookScript, resolveHookScriptPath, runHookScriptHook, } from '../lib/hook-script.js';
|
|
32
33
|
import { areConfiguredPathsEquivalent, deriveConfiguredSourcePath, deriveConfiguredStoragePath, inferConfiguredAppPathFromLegacyConfig, } from '../lib/env-paths.js';
|
|
33
34
|
import Download, { defaultDockerRegistryForLang } from './download.js';
|
|
34
35
|
import EnvAdd from "./env/add.js";
|
|
@@ -472,7 +473,10 @@ export default class Install extends Command {
|
|
|
472
473
|
description: 'Skip the download step and reuse an existing local app directory or Docker image',
|
|
473
474
|
default: false,
|
|
474
475
|
}),
|
|
475
|
-
|
|
476
|
+
'hook-script': Flags.string({
|
|
477
|
+
description: 'Hook module copied to <appPath>/.nb/hooks.mjs and run before npm/git dependency installation.',
|
|
478
|
+
}),
|
|
479
|
+
...omitKeys(Download.flags, ['yes', 'hook-script']),
|
|
476
480
|
};
|
|
477
481
|
/** Environment name only: run before {@link Install.prompts} (see `run`). */
|
|
478
482
|
static envPrompts = {
|
|
@@ -1095,6 +1099,7 @@ export default class Install extends Command {
|
|
|
1095
1099
|
const appPort = Install.toOptionalPromptString(config.appPort);
|
|
1096
1100
|
const storagePath = Install.toOptionalPromptString(config.storagePath);
|
|
1097
1101
|
const appPublicPath = Install.toOptionalPromptString(config.appPublicPath);
|
|
1102
|
+
const hookScript = Install.toOptionalPromptString(config.hookScript);
|
|
1098
1103
|
const apiBaseUrl = Install.toOptionalPromptString(config.apiBaseUrl);
|
|
1099
1104
|
const downloadVersion = Install.toOptionalPromptString(config.downloadVersion);
|
|
1100
1105
|
const dockerRegistry = Install.toOptionalPromptString(config.dockerRegistry);
|
|
@@ -1125,6 +1130,7 @@ export default class Install extends Command {
|
|
|
1125
1130
|
...(appPort ? { appPort } : {}),
|
|
1126
1131
|
...(storagePath ? { storagePath } : {}),
|
|
1127
1132
|
...(appPublicPath ? { appPublicPath } : {}),
|
|
1133
|
+
...(hookScript ? { hookScript } : {}),
|
|
1128
1134
|
};
|
|
1129
1135
|
const downloadPreset = {
|
|
1130
1136
|
...(source ? { source } : {}),
|
|
@@ -1916,6 +1922,12 @@ export default class Install extends Command {
|
|
|
1916
1922
|
if (results.buildDts) {
|
|
1917
1923
|
argv.push('--build-dts');
|
|
1918
1924
|
}
|
|
1925
|
+
Install.pushDownloadArgIfValue(argv, '--hook-script', results.hookScript);
|
|
1926
|
+
Install.pushDownloadArgIfValue(argv, '--hook-phase', results.hookPhase);
|
|
1927
|
+
Install.pushDownloadArgIfValue(argv, '--hook-command', results.hookCommand);
|
|
1928
|
+
Install.pushDownloadArgIfValue(argv, '--hook-env-name', results.hookEnvName);
|
|
1929
|
+
Install.pushDownloadArgIfValue(argv, '--hook-app-path', results.hookAppPath);
|
|
1930
|
+
Install.pushDownloadArgIfValue(argv, '--hook-storage-path', results.hookStoragePath);
|
|
1919
1931
|
return argv;
|
|
1920
1932
|
}
|
|
1921
1933
|
static resolveLocalProjectRoot(params) {
|
|
@@ -1942,6 +1954,40 @@ export default class Install extends Command {
|
|
|
1942
1954
|
buildDts: false,
|
|
1943
1955
|
};
|
|
1944
1956
|
}
|
|
1957
|
+
static resolveAbsoluteAppPath(envName, appResults) {
|
|
1958
|
+
const configuredAppPath = resolveConfiguredAppPathValue(appResults) ?? defaultInstallAppPath(envName);
|
|
1959
|
+
return resolveConfiguredEnvPath(configuredAppPath) ?? resolveEnvRelativePath(defaultInstallAppPath(envName));
|
|
1960
|
+
}
|
|
1961
|
+
static resolveAbsoluteStoragePath(envName, appResults) {
|
|
1962
|
+
const configuredStoragePath = resolveConfiguredStoragePathValue(appResults, envName);
|
|
1963
|
+
return (resolveConfiguredEnvPath(configuredStoragePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(envName)));
|
|
1964
|
+
}
|
|
1965
|
+
async prepareHookScriptForInstall(params) {
|
|
1966
|
+
const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
|
|
1967
|
+
const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
|
|
1968
|
+
const inputHookScript = Install.toOptionalPromptString(params.hookScript);
|
|
1969
|
+
if (inputHookScript) {
|
|
1970
|
+
params.appResults.hookScript = await persistHookScript({
|
|
1971
|
+
sourcePath: inputHookScript,
|
|
1972
|
+
appPath,
|
|
1973
|
+
});
|
|
1974
|
+
printInfo(`Saved hook script to ${path.join(appPath, String(params.appResults.hookScript))}.`);
|
|
1975
|
+
}
|
|
1976
|
+
const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
|
|
1977
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
1978
|
+
appPath,
|
|
1979
|
+
hookScript: savedHookScript,
|
|
1980
|
+
});
|
|
1981
|
+
if (!hookScriptPath) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
params.downloadResults.hookScript = hookScriptPath;
|
|
1985
|
+
params.downloadResults.hookPhase = 'init';
|
|
1986
|
+
params.downloadResults.hookCommand = 'init';
|
|
1987
|
+
params.downloadResults.hookEnvName = params.envName;
|
|
1988
|
+
params.downloadResults.hookAppPath = appPath;
|
|
1989
|
+
params.downloadResults.hookStoragePath = storagePath;
|
|
1990
|
+
}
|
|
1945
1991
|
commandStdio(verbose) {
|
|
1946
1992
|
return verbose ? 'inherit' : 'ignore';
|
|
1947
1993
|
}
|
|
@@ -2011,7 +2057,9 @@ export default class Install extends Command {
|
|
|
2011
2057
|
const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
2012
2058
|
const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
|
|
2013
2059
|
const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
|
|
2060
|
+
const lifecycleEnvVars = managedAppLifecycleEnvVars();
|
|
2014
2061
|
const env = {
|
|
2062
|
+
...lifecycleEnvVars,
|
|
2015
2063
|
STORAGE_PATH: storagePath,
|
|
2016
2064
|
APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
|
|
2017
2065
|
APP_KEY: appKey,
|
|
@@ -2216,6 +2264,44 @@ export default class Install extends Command {
|
|
|
2216
2264
|
}
|
|
2217
2265
|
await this.config.runCommand('env:update', [params.envName]);
|
|
2218
2266
|
}
|
|
2267
|
+
async runInstallHookIfNeeded(params) {
|
|
2268
|
+
const appPath = Install.resolveAbsoluteAppPath(params.envName, params.appResults);
|
|
2269
|
+
const savedHookScript = Install.toOptionalPromptString(params.appResults.hookScript);
|
|
2270
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
2271
|
+
appPath,
|
|
2272
|
+
hookScript: savedHookScript,
|
|
2273
|
+
});
|
|
2274
|
+
if (!hookScriptPath) {
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const storagePath = Install.resolveAbsoluteStoragePath(params.envName, params.appResults);
|
|
2278
|
+
const sourcePath = params.projectRoot ??
|
|
2279
|
+
resolveConfiguredEnvPath(resolveConfiguredSourcePathValue(params.appResults, params.envName)) ??
|
|
2280
|
+
path.join(appPath, 'source');
|
|
2281
|
+
const context = buildHookContext({
|
|
2282
|
+
phase: 'init',
|
|
2283
|
+
command: 'init',
|
|
2284
|
+
envName: params.envName,
|
|
2285
|
+
source: params.source,
|
|
2286
|
+
version: downloadResultsValue(params.downloadResults, 'version'),
|
|
2287
|
+
appPath,
|
|
2288
|
+
sourcePath,
|
|
2289
|
+
storagePath,
|
|
2290
|
+
hookScript: savedHookScript,
|
|
2291
|
+
envConfig: Install.buildSavedEnvConfig(params, {
|
|
2292
|
+
defaultApiHost: params.defaultApiHost,
|
|
2293
|
+
}),
|
|
2294
|
+
});
|
|
2295
|
+
if (!context) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
this.log(`Running hook ${params.hookName}: ${hookScriptPath}`);
|
|
2299
|
+
await runHookScriptHook({
|
|
2300
|
+
hookScriptPath,
|
|
2301
|
+
hookName: params.hookName,
|
|
2302
|
+
context,
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2219
2305
|
static buildSavedEnvConfig(params, options = {}) {
|
|
2220
2306
|
const appPath = resolveConfiguredAppPathValue(params.appResults);
|
|
2221
2307
|
const appRootPath = Install.toOptionalPromptString(params.appResults.appRootPath);
|
|
@@ -2247,6 +2333,7 @@ export default class Install extends Command {
|
|
|
2247
2333
|
devDependencies: downloadResultsValue(params.downloadResults, 'devDependencies'),
|
|
2248
2334
|
build: downloadResultsValue(params.downloadResults, 'build'),
|
|
2249
2335
|
buildDts: downloadResultsValue(params.downloadResults, 'buildDts'),
|
|
2336
|
+
hookScript: params.appResults.hookScript,
|
|
2250
2337
|
...(appPath ? { appPath } : {}),
|
|
2251
2338
|
...(appRootPath && !areConfiguredPathsEquivalent(appRootPath, derivedAppRootPath) ? { appRootPath } : {}),
|
|
2252
2339
|
appPort,
|
|
@@ -2311,6 +2398,9 @@ export default class Install extends Command {
|
|
|
2311
2398
|
yesInitialValues: { resume: parsed.resume },
|
|
2312
2399
|
yes,
|
|
2313
2400
|
});
|
|
2401
|
+
if (resumePreset?.appPreset?.hookScript !== undefined && appResults.hookScript === undefined) {
|
|
2402
|
+
appResults.hookScript = resumePreset.appPreset.hookScript;
|
|
2403
|
+
}
|
|
2314
2404
|
const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
|
|
2315
2405
|
downloadOpts.values = {
|
|
2316
2406
|
...(resumePreset?.downloadPreset ?? {}),
|
|
@@ -2440,6 +2530,12 @@ export default class Install extends Command {
|
|
|
2440
2530
|
appResults,
|
|
2441
2531
|
});
|
|
2442
2532
|
appResults.setupState = 'prepared';
|
|
2533
|
+
await this.prepareHookScriptForInstall({
|
|
2534
|
+
envName,
|
|
2535
|
+
appResults,
|
|
2536
|
+
downloadResults,
|
|
2537
|
+
hookScript: parsed['hook-script'],
|
|
2538
|
+
});
|
|
2443
2539
|
const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
|
|
2444
2540
|
const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
|
|
2445
2541
|
const dockerNetworkName = usesDockerResources
|
|
@@ -2505,6 +2601,17 @@ export default class Install extends Command {
|
|
|
2505
2601
|
printInfo('Application image ready.');
|
|
2506
2602
|
}
|
|
2507
2603
|
if (!parsed['prepare-only']) {
|
|
2604
|
+
await this.runInstallHookIfNeeded({
|
|
2605
|
+
hookName: 'beforeAppInstall',
|
|
2606
|
+
envName,
|
|
2607
|
+
source,
|
|
2608
|
+
appResults,
|
|
2609
|
+
downloadResults,
|
|
2610
|
+
dbResults,
|
|
2611
|
+
rootResults,
|
|
2612
|
+
envAddResults,
|
|
2613
|
+
defaultApiHost,
|
|
2614
|
+
});
|
|
2508
2615
|
dockerAppPlan = await this.installDockerApp({
|
|
2509
2616
|
envName,
|
|
2510
2617
|
dockerNetworkName,
|
|
@@ -2523,7 +2630,7 @@ export default class Install extends Command {
|
|
|
2523
2630
|
}
|
|
2524
2631
|
else if (source === 'npm' || source === 'git') {
|
|
2525
2632
|
const localSource = source === 'npm' ? 'npm' : 'git';
|
|
2526
|
-
const projectRoot = parsed['skip-download']
|
|
2633
|
+
const projectRoot = parsed['skip-download'] || parsed['prepare-only']
|
|
2527
2634
|
? Install.resolveLocalProjectRoot({
|
|
2528
2635
|
envName,
|
|
2529
2636
|
appResults,
|
|
@@ -2535,10 +2642,22 @@ export default class Install extends Command {
|
|
|
2535
2642
|
downloadResults,
|
|
2536
2643
|
verbose: parsed.verbose,
|
|
2537
2644
|
});
|
|
2538
|
-
if (!parsed['skip-download']) {
|
|
2645
|
+
if (!parsed['skip-download'] && !parsed['prepare-only']) {
|
|
2539
2646
|
printInfo('Application files ready.');
|
|
2540
2647
|
}
|
|
2541
2648
|
if (!parsed['prepare-only']) {
|
|
2649
|
+
await this.runInstallHookIfNeeded({
|
|
2650
|
+
hookName: 'beforeAppInstall',
|
|
2651
|
+
envName,
|
|
2652
|
+
source,
|
|
2653
|
+
appResults,
|
|
2654
|
+
downloadResults,
|
|
2655
|
+
dbResults,
|
|
2656
|
+
rootResults,
|
|
2657
|
+
envAddResults,
|
|
2658
|
+
projectRoot,
|
|
2659
|
+
defaultApiHost,
|
|
2660
|
+
});
|
|
2542
2661
|
localAppPlan = await this.startLocalApp({
|
|
2543
2662
|
envName,
|
|
2544
2663
|
source: localSource,
|
|
@@ -2573,6 +2692,18 @@ export default class Install extends Command {
|
|
|
2573
2692
|
});
|
|
2574
2693
|
printInfo(`NocoBase is ready at ${formatInstallDisplayUrl(displayApiBaseUrl)}`);
|
|
2575
2694
|
appResults.setupState = 'installed';
|
|
2695
|
+
await this.runInstallHookIfNeeded({
|
|
2696
|
+
hookName: 'afterAppStart',
|
|
2697
|
+
envName,
|
|
2698
|
+
source,
|
|
2699
|
+
appResults,
|
|
2700
|
+
downloadResults,
|
|
2701
|
+
dbResults,
|
|
2702
|
+
rootResults,
|
|
2703
|
+
envAddResults,
|
|
2704
|
+
projectRoot: localAppPlan?.projectRoot,
|
|
2705
|
+
defaultApiHost,
|
|
2706
|
+
});
|
|
2576
2707
|
}
|
|
2577
2708
|
if (dockerAppPlan || localAppPlan || builtinDbPlan) {
|
|
2578
2709
|
await this.saveInstalledEnv({
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Command, Flags } from '@oclif/core';
|
|
10
10
|
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
11
|
-
import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
|
|
11
|
+
import { ensureLocalPostinstall, ensureNpmSourceDevDependencies } from '../../lib/app-managed-resources.js';
|
|
12
12
|
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
|
|
13
|
+
import { findAvailableTcpPort } from '../../lib/prompt-validators.js';
|
|
13
14
|
import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
|
|
14
15
|
function formatUnsupportedRuntimeMessage(kind, envName) {
|
|
15
16
|
if (kind === 'docker') {
|
|
@@ -129,10 +130,7 @@ export default class SourceDev extends Command {
|
|
|
129
130
|
this.error(formatUnsupportedRuntimeMessage(runtime.kind, runtime.envName));
|
|
130
131
|
}
|
|
131
132
|
announceTargetEnv(runtime.envName);
|
|
132
|
-
const devPort = flags.port ||
|
|
133
|
-
(runtime.env.appPort !== undefined && runtime.env.appPort !== null
|
|
134
|
-
? String(runtime.env.appPort).trim()
|
|
135
|
-
: undefined);
|
|
133
|
+
const devPort = flags.port?.trim() || (await findAvailableTcpPort());
|
|
136
134
|
const appUrl = appUrlForPort(devPort);
|
|
137
135
|
if (await isAppAlreadyRunning(appUrl)) {
|
|
138
136
|
this.error([
|
|
@@ -160,6 +158,12 @@ export default class SourceDev extends Command {
|
|
|
160
158
|
}
|
|
161
159
|
printInfo(`Starting NocoBase dev mode for "${runtime.envName}" from ${runtime.projectRoot}. Press Ctrl+C to stop.`);
|
|
162
160
|
try {
|
|
161
|
+
await ensureNpmSourceDevDependencies(runtime, {
|
|
162
|
+
onStartTask: startTask,
|
|
163
|
+
onSucceedTask: succeedTask,
|
|
164
|
+
onFailTask: failTask,
|
|
165
|
+
verbose: true,
|
|
166
|
+
});
|
|
163
167
|
await ensureLocalPostinstall(runtime, {
|
|
164
168
|
onStartTask: startTask,
|
|
165
169
|
onSucceedTask: succeedTask,
|
|
@@ -13,6 +13,9 @@ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
|
|
|
13
13
|
import { runPromptCatalog, } from "../../lib/prompt-catalog.js";
|
|
14
14
|
import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, resolveCliLocale, translateCli, } from "../../lib/cli-locale.js";
|
|
15
15
|
import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_REGISTRY_ZH_CN, resolveDockerImageRef, } from "../../lib/docker-image.js";
|
|
16
|
+
import { getEnv } from '../../lib/auth-store.js';
|
|
17
|
+
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
18
|
+
import { buildBeforeDependencyInstallHookContext, runBeforeDependencyInstallHook, } from '../../lib/hook-script.js';
|
|
16
19
|
import { run } from "../../lib/run-npm.js";
|
|
17
20
|
import { printVerbose, setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
|
|
18
21
|
const DEFAULT_DOCKER_PLATFORM = 'auto';
|
|
@@ -231,6 +234,26 @@ export default class SourceDownload extends Command {
|
|
|
231
234
|
'npm-registry': Flags.string({
|
|
232
235
|
description: 'npm registry for npm/git downloads and dependency installation.',
|
|
233
236
|
}),
|
|
237
|
+
'hook-script': Flags.string({
|
|
238
|
+
description: 'Hook module to run after npm scaffold or git clone and before dependency installation.',
|
|
239
|
+
}),
|
|
240
|
+
'hook-phase': Flags.string({
|
|
241
|
+
hidden: true,
|
|
242
|
+
options: ['init', 'upgrade', 'restore', 'source-download', 'app-start'],
|
|
243
|
+
}),
|
|
244
|
+
'hook-command': Flags.string({
|
|
245
|
+
hidden: true,
|
|
246
|
+
options: ['init', 'source:download', 'app:start', 'app:restart', 'app:upgrade'],
|
|
247
|
+
}),
|
|
248
|
+
'hook-env-name': Flags.string({
|
|
249
|
+
hidden: true,
|
|
250
|
+
}),
|
|
251
|
+
'hook-app-path': Flags.string({
|
|
252
|
+
hidden: true,
|
|
253
|
+
}),
|
|
254
|
+
'hook-storage-path': Flags.string({
|
|
255
|
+
hidden: true,
|
|
256
|
+
}),
|
|
234
257
|
build: Flags.boolean({
|
|
235
258
|
allowNo: true,
|
|
236
259
|
description: 'Build npm/git source after dependencies are installed.',
|
|
@@ -573,6 +596,9 @@ export default class SourceDownload extends Command {
|
|
|
573
596
|
const npmRegistryRaw = results.npmRegistry !== undefined ? String(results.npmRegistry) : flags['npm-registry'] ?? '';
|
|
574
597
|
const npmRegistry = npmRegistryRaw.trim() || undefined;
|
|
575
598
|
const npmDevDependencies = devDependencies ?? false;
|
|
599
|
+
const hookScript = flags['hook-script']?.trim()
|
|
600
|
+
? path.resolve(process.cwd(), flags['hook-script'].trim())
|
|
601
|
+
: undefined;
|
|
576
602
|
return {
|
|
577
603
|
source,
|
|
578
604
|
version,
|
|
@@ -586,6 +612,14 @@ export default class SourceDownload extends Command {
|
|
|
586
612
|
...(source === 'docker' ? { 'docker-platform': dockerPlatform } : {}),
|
|
587
613
|
...(source === 'docker' ? { 'docker-save': dockerSave } : {}),
|
|
588
614
|
...(npmRegistry ? { 'npm-registry': npmRegistry } : {}),
|
|
615
|
+
...(hookScript ? { 'hook-script': hookScript } : {}),
|
|
616
|
+
...(flags['hook-phase'] ? { 'hook-phase': flags['hook-phase'] } : {}),
|
|
617
|
+
...(flags['hook-command'] ? { 'hook-command': flags['hook-command'] } : {}),
|
|
618
|
+
...(flags['hook-env-name'] ? { 'hook-env-name': flags['hook-env-name'].trim() } : {}),
|
|
619
|
+
...(flags['hook-app-path'] ? { 'hook-app-path': path.resolve(process.cwd(), flags['hook-app-path']) } : {}),
|
|
620
|
+
...(flags['hook-storage-path']
|
|
621
|
+
? { 'hook-storage-path': path.resolve(process.cwd(), flags['hook-storage-path']) }
|
|
622
|
+
: {}),
|
|
589
623
|
};
|
|
590
624
|
}
|
|
591
625
|
async resolveDownloadFlags(flags) {
|
|
@@ -718,6 +752,36 @@ export default class SourceDownload extends Command {
|
|
|
718
752
|
}
|
|
719
753
|
return argv;
|
|
720
754
|
}
|
|
755
|
+
async runBeforeDependencyInstallHookIfNeeded(flags, projectRoot) {
|
|
756
|
+
const hookScript = flags['hook-script']?.trim();
|
|
757
|
+
if (!hookScript || (flags.source !== 'npm' && flags.source !== 'git')) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const envName = flags['hook-env-name']?.trim() || '';
|
|
761
|
+
const env = envName ? await getEnv(envName, { scope: resolveDefaultConfigScope() }) : undefined;
|
|
762
|
+
const appPath = flags['hook-app-path']?.trim() || projectRoot;
|
|
763
|
+
const storagePath = flags['hook-storage-path']?.trim() || path.join(appPath, 'storage');
|
|
764
|
+
const context = buildBeforeDependencyInstallHookContext({
|
|
765
|
+
phase: flags['hook-phase'] ?? 'source-download',
|
|
766
|
+
command: flags['hook-command'] ?? 'source:download',
|
|
767
|
+
envName,
|
|
768
|
+
source: flags.source,
|
|
769
|
+
version: flags.version,
|
|
770
|
+
appPath,
|
|
771
|
+
sourcePath: projectRoot,
|
|
772
|
+
storagePath,
|
|
773
|
+
hookScript,
|
|
774
|
+
envConfig: env?.config ?? {},
|
|
775
|
+
});
|
|
776
|
+
if (!context) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.log(`Running hook before dependency install: ${hookScript}`);
|
|
780
|
+
await runBeforeDependencyInstallHook({
|
|
781
|
+
hookScriptPath: hookScript,
|
|
782
|
+
context,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
721
785
|
async downloadFromDocker(flags) {
|
|
722
786
|
const imageRef = resolveDockerImageRef(flags['docker-registry'], flags.version, {
|
|
723
787
|
defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
|
|
@@ -777,6 +841,7 @@ export default class SourceDownload extends Command {
|
|
|
777
841
|
errorName: 'npx create-nocobase-app',
|
|
778
842
|
loadingMessage: 'Creating the app scaffold',
|
|
779
843
|
});
|
|
844
|
+
await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
|
|
780
845
|
const installArgs = ['install'];
|
|
781
846
|
if (!flags['dev-dependencies']) {
|
|
782
847
|
installArgs.push('--production');
|
|
@@ -820,6 +885,7 @@ export default class SourceDownload extends Command {
|
|
|
820
885
|
});
|
|
821
886
|
const projectRoot = path.resolve(process.cwd(), outputDir);
|
|
822
887
|
const registryEnv = this.npmRegistryEnv(flags);
|
|
888
|
+
await this.runBeforeDependencyInstallHookIfNeeded(flags, projectRoot);
|
|
823
889
|
this.log(`Installing dependencies in ${projectRoot}`);
|
|
824
890
|
await this.runExternalCommand('yarn', ['install'], {
|
|
825
891
|
...this.runOptionsWithCwd(projectRoot, registryEnv),
|
|
@@ -6,13 +6,16 @@
|
|
|
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 {
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
10
12
|
import { resolveAppPublicPath } from './app-public-path.js';
|
|
11
13
|
import { dockerContainerExists, managedAppLifecycleEnvVars, runLocalNocoBaseCommand, startDockerContainer, } from './app-runtime.js';
|
|
12
14
|
import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
|
|
13
15
|
import { resolveConfiguredStoragePath } from './env-paths.js';
|
|
14
16
|
import { resolveDockerEnvFileArg } from "./docker-env-file.js";
|
|
15
17
|
import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "./docker-image.js";
|
|
18
|
+
import { resolveHookScriptPath } from './hook-script.js';
|
|
16
19
|
import { commandSucceeds, ensureDockerDaemonRunning, run } from './run-npm.js';
|
|
17
20
|
import Install from '../commands/install.js';
|
|
18
21
|
const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
|
|
@@ -90,6 +93,15 @@ function formatLocalPostinstallFailure(envName, message) {
|
|
|
90
93
|
`Details: ${message}`,
|
|
91
94
|
].join('\n');
|
|
92
95
|
}
|
|
96
|
+
function formatNpmSourceDevDependenciesFailure(envName, projectRoot, message) {
|
|
97
|
+
return [
|
|
98
|
+
`Couldn't prepare source dev dependencies for "${envName}".`,
|
|
99
|
+
'`nb source dev` requires @nocobase/devtools in npm source envs.',
|
|
100
|
+
`Source directory: ${projectRoot}`,
|
|
101
|
+
`Run \`cd ${projectRoot} && yarn install\` after fixing package.json, then try again.`,
|
|
102
|
+
`Details: ${message}`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
93
105
|
function formatSavedDockerSettingsIncomplete(envName, missing) {
|
|
94
106
|
return [
|
|
95
107
|
`Can't start NocoBase for "${envName}" yet.`,
|
|
@@ -114,6 +126,45 @@ async function localProjectHasFiles(projectRoot) {
|
|
|
114
126
|
return false;
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
function isRecord(value) {
|
|
130
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
131
|
+
}
|
|
132
|
+
async function readPackageJson(projectRoot) {
|
|
133
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
134
|
+
const content = await readFile(packageJsonPath, 'utf-8');
|
|
135
|
+
const parsed = JSON.parse(content);
|
|
136
|
+
if (!isRecord(parsed)) {
|
|
137
|
+
throw new Error(`${packageJsonPath} must contain a JSON object.`);
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
function getStringDependency(packageJson, section, packageName) {
|
|
142
|
+
const dependencies = packageJson[section];
|
|
143
|
+
if (!isRecord(dependencies)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const version = dependencies[packageName];
|
|
147
|
+
return typeof version === 'string' && version.trim() ? version.trim() : undefined;
|
|
148
|
+
}
|
|
149
|
+
function ensureDevDependencies(packageJson) {
|
|
150
|
+
const devDependencies = packageJson.devDependencies;
|
|
151
|
+
if (devDependencies === undefined) {
|
|
152
|
+
const next = {};
|
|
153
|
+
packageJson.devDependencies = next;
|
|
154
|
+
return next;
|
|
155
|
+
}
|
|
156
|
+
if (!isRecord(devDependencies)) {
|
|
157
|
+
throw new Error('package.json devDependencies must be an object.');
|
|
158
|
+
}
|
|
159
|
+
return devDependencies;
|
|
160
|
+
}
|
|
161
|
+
function hasNpmSourceDevtools(projectRoot) {
|
|
162
|
+
return existsSync(path.join(projectRoot, 'node_modules', '@nocobase', 'devtools', 'package.json'));
|
|
163
|
+
}
|
|
164
|
+
function npmRegistryEnv(runtime) {
|
|
165
|
+
const npmRegistry = String(runtime.env.config?.npmRegistry ?? '').trim();
|
|
166
|
+
return npmRegistry ? { npm_config_registry: npmRegistry } : undefined;
|
|
167
|
+
}
|
|
117
168
|
export async function buildSavedDockerRunArgs(runtime, options) {
|
|
118
169
|
const config = runtime.env.config ?? {};
|
|
119
170
|
const storagePath = trimValue(resolveConfiguredStoragePath(config));
|
|
@@ -302,6 +353,13 @@ export function buildSavedLocalDownloadArgv(runtime, options) {
|
|
|
302
353
|
if (config.buildDts === true) {
|
|
303
354
|
argv.push('--build-dts');
|
|
304
355
|
}
|
|
356
|
+
const hookScriptPath = resolveHookScriptPath({
|
|
357
|
+
appPath: runtime.env.appPath,
|
|
358
|
+
hookScript: config.hookScript,
|
|
359
|
+
});
|
|
360
|
+
if (hookScriptPath) {
|
|
361
|
+
argv.push('--hook-script', hookScriptPath, '--hook-phase', options?.hookPhase ?? 'restore', '--hook-command', options?.hookCommand ?? 'app:start', '--hook-env-name', runtime.envName, '--hook-app-path', runtime.env.appPath, '--hook-storage-path', runtime.env.storagePath);
|
|
362
|
+
}
|
|
305
363
|
return argv;
|
|
306
364
|
}
|
|
307
365
|
export async function ensureSavedLocalSource(runtime, runCommand, options) {
|
|
@@ -313,6 +371,8 @@ export async function ensureSavedLocalSource(runtime, runCommand, options) {
|
|
|
313
371
|
try {
|
|
314
372
|
await runCommand('source:download', buildSavedLocalDownloadArgv(runtime, {
|
|
315
373
|
verbose: options?.verbose,
|
|
374
|
+
hookPhase: options?.hookPhase,
|
|
375
|
+
hookCommand: options?.hookCommand,
|
|
316
376
|
}));
|
|
317
377
|
options?.onSucceedTask?.(`NocoBase files are ready for "${runtime.envName}".`);
|
|
318
378
|
}
|
|
@@ -335,3 +395,43 @@ export async function ensureLocalPostinstall(runtime, options) {
|
|
|
335
395
|
throw new Error(formatLocalPostinstallFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
|
|
336
396
|
}
|
|
337
397
|
}
|
|
398
|
+
export async function ensureNpmSourceDevDependencies(runtime, options) {
|
|
399
|
+
if (runtime.source !== 'npm') {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
let taskStarted = false;
|
|
403
|
+
try {
|
|
404
|
+
const packageJson = await readPackageJson(runtime.projectRoot);
|
|
405
|
+
const appVersion = getStringDependency(packageJson, 'dependencies', '@nocobase/app');
|
|
406
|
+
const devtoolsVersion = getStringDependency(packageJson, 'devDependencies', '@nocobase/devtools');
|
|
407
|
+
let updatedPackageJson = false;
|
|
408
|
+
if (!devtoolsVersion) {
|
|
409
|
+
if (!appVersion) {
|
|
410
|
+
throw new Error('Cannot determine @nocobase/devtools version because dependencies["@nocobase/app"] is missing.');
|
|
411
|
+
}
|
|
412
|
+
const devDependencies = ensureDevDependencies(packageJson);
|
|
413
|
+
devDependencies['@nocobase/devtools'] = appVersion;
|
|
414
|
+
updatedPackageJson = true;
|
|
415
|
+
await writeFile(path.join(runtime.projectRoot, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8');
|
|
416
|
+
}
|
|
417
|
+
const needsInstall = updatedPackageJson || !hasNpmSourceDevtools(runtime.projectRoot);
|
|
418
|
+
if (!needsInstall) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
options?.onStartTask?.(`Preparing source dev dependencies for "${runtime.envName}"...`);
|
|
422
|
+
taskStarted = true;
|
|
423
|
+
await run('yarn', ['install'], {
|
|
424
|
+
cwd: runtime.projectRoot,
|
|
425
|
+
env: npmRegistryEnv(runtime),
|
|
426
|
+
errorName: 'yarn install',
|
|
427
|
+
stdio: commandStdio(options?.verbose),
|
|
428
|
+
});
|
|
429
|
+
options?.onSucceedTask?.(`Source dev dependencies are ready for "${runtime.envName}".`);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (taskStarted) {
|
|
433
|
+
options?.onFailTask?.(`Failed to prepare source dev dependencies for "${runtime.envName}".`);
|
|
434
|
+
}
|
|
435
|
+
throw new Error(formatNpmSourceDevDependenciesFailure(runtime.envName, runtime.projectRoot, error instanceof Error ? error.message : String(error)));
|
|
436
|
+
}
|
|
437
|
+
}
|
package/dist/lib/env-config.js
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
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 { copyFile, mkdir } from 'node:fs/promises';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export const ENV_HOOK_SCRIPT_CONFIG_PATH = '.nb/hooks.mjs';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { spawn } = require('node:child_process');
|
|
15
|
+
function trimValue(value) {
|
|
16
|
+
return String(value ?? '').trim();
|
|
17
|
+
}
|
|
18
|
+
function normalizeHookPhase(value) {
|
|
19
|
+
const text = trimValue(value);
|
|
20
|
+
if (text === 'init' || text === 'upgrade' || text === 'restore' || text === 'source-download' || text === 'app-start') {
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
return 'init';
|
|
24
|
+
}
|
|
25
|
+
function normalizeHookCommand(value) {
|
|
26
|
+
const text = trimValue(value);
|
|
27
|
+
if (text === 'source:download' || text === 'app:start' || text === 'app:restart' || text === 'app:upgrade') {
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
return 'init';
|
|
31
|
+
}
|
|
32
|
+
function normalizeHookSource(value) {
|
|
33
|
+
const text = trimValue(value);
|
|
34
|
+
if (text === 'npm' || text === 'git' || text === 'docker') {
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function isDependencyHookSource(source) {
|
|
40
|
+
return source === 'npm' || source === 'git';
|
|
41
|
+
}
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
export function resolveHookScriptPath(params) {
|
|
46
|
+
const hookScript = trimValue(params.hookScript);
|
|
47
|
+
if (!hookScript) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (path.isAbsolute(hookScript)) {
|
|
51
|
+
return hookScript;
|
|
52
|
+
}
|
|
53
|
+
const appPath = trimValue(params.appPath);
|
|
54
|
+
if (!appPath) {
|
|
55
|
+
return hookScript;
|
|
56
|
+
}
|
|
57
|
+
const usesWindowsSeparators = appPath.includes('\\') || /^[a-zA-Z]:([\\/]|$)/.test(appPath) || appPath.startsWith('\\\\');
|
|
58
|
+
return usesWindowsSeparators ? path.win32.join(appPath, hookScript) : path.posix.join(appPath, hookScript);
|
|
59
|
+
}
|
|
60
|
+
export async function persistHookScript(params) {
|
|
61
|
+
const sourcePath = path.resolve(params.sourcePath);
|
|
62
|
+
const targetPath = path.join(params.appPath, ENV_HOOK_SCRIPT_CONFIG_PATH);
|
|
63
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
64
|
+
if (path.resolve(sourcePath) !== path.resolve(targetPath)) {
|
|
65
|
+
await copyFile(sourcePath, targetPath);
|
|
66
|
+
}
|
|
67
|
+
return ENV_HOOK_SCRIPT_CONFIG_PATH;
|
|
68
|
+
}
|
|
69
|
+
const hookRunnerScript = `
|
|
70
|
+
import { pathToFileURL } from 'node:url';
|
|
71
|
+
|
|
72
|
+
const knownHookNames = ['beforeDependencyInstall', 'beforeAppInstall', 'afterAppStart'];
|
|
73
|
+
const [, hookScriptPath, hookName, contextJson] = process.argv;
|
|
74
|
+
const url = pathToFileURL(hookScriptPath);
|
|
75
|
+
url.searchParams.set('t', String(Date.now()));
|
|
76
|
+
|
|
77
|
+
const imported = await import(url.href);
|
|
78
|
+
const hooks = imported.default ?? imported;
|
|
79
|
+
|
|
80
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
|
|
81
|
+
throw new Error('Hook script must export an object.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const knownHookName of knownHookNames) {
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(hooks, knownHookName) && typeof hooks[knownHookName] !== 'function') {
|
|
86
|
+
throw new Error(\`Hook "\${knownHookName}" must be a function.\`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hook = hooks[hookName];
|
|
91
|
+
if (typeof hook === 'function') {
|
|
92
|
+
await hook(JSON.parse(contextJson));
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
async function runHookInSubprocess(params) {
|
|
96
|
+
await new Promise((resolve, reject) => {
|
|
97
|
+
const child = spawn(process.execPath, ['--input-type=module', '--eval', hookRunnerScript, params.hookScriptPath, params.hookName, JSON.stringify(params.context)], {
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
let stdout = '';
|
|
101
|
+
let stderr = '';
|
|
102
|
+
child.stdout?.on?.('data', (chunk) => {
|
|
103
|
+
stdout += String(chunk);
|
|
104
|
+
});
|
|
105
|
+
child.stderr?.on?.('data', (chunk) => {
|
|
106
|
+
stderr += String(chunk);
|
|
107
|
+
});
|
|
108
|
+
child.once('error', reject);
|
|
109
|
+
child.once('close', (code) => {
|
|
110
|
+
if (code === 0) {
|
|
111
|
+
resolve();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const output = stderr.trim() || stdout.trim();
|
|
115
|
+
reject(new Error(output || `Hook process exited with code ${code ?? 'unknown'}.`));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export function buildHookContext(params) {
|
|
120
|
+
const source = normalizeHookSource(params.source);
|
|
121
|
+
if (!source) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const version = trimValue(params.version);
|
|
125
|
+
return {
|
|
126
|
+
phase: normalizeHookPhase(params.phase),
|
|
127
|
+
command: normalizeHookCommand(params.command),
|
|
128
|
+
envName: trimValue(params.envName),
|
|
129
|
+
source,
|
|
130
|
+
...(version ? { version } : {}),
|
|
131
|
+
appPath: params.appPath,
|
|
132
|
+
sourcePath: params.sourcePath,
|
|
133
|
+
storagePath: params.storagePath,
|
|
134
|
+
hookScript: params.hookScript,
|
|
135
|
+
envConfig: { ...(params.envConfig ?? {}) },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function buildBeforeDependencyInstallHookContext(params) {
|
|
139
|
+
const context = buildHookContext(params);
|
|
140
|
+
if (!context || !isDependencyHookSource(context.source)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return context;
|
|
144
|
+
}
|
|
145
|
+
export async function runHookScriptHook(params) {
|
|
146
|
+
try {
|
|
147
|
+
await runHookInSubprocess(params);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
throw new Error([`Hook script failed: ${params.hookScriptPath}`, `Hook stage: ${params.hookName}`, `Details: ${message}`].join('\n'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function runBeforeDependencyInstallHook(params) {
|
|
155
|
+
await runHookScriptHook({
|
|
156
|
+
hookScriptPath: params.hookScriptPath,
|
|
157
|
+
hookName: 'beforeDependencyInstall',
|
|
158
|
+
context: params.context,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -232,7 +232,7 @@ export async function publishSourceSnapshot(params) {
|
|
|
232
232
|
version,
|
|
233
233
|
stdio,
|
|
234
234
|
});
|
|
235
|
-
await run('yarn', ['lerna', 'publish', 'from-package', '--registry', npmRegistry, '--dist-tag', 'local', '--yes', '--no-verify-access', '--git-head', gitSha], {
|
|
235
|
+
await run('yarn', ['lerna', 'publish', 'from-package', '--registry', npmRegistry, '--dist-tag', 'local', '--yes', '--no-verify-access', '--no-git-reset', '--git-head', gitSha], {
|
|
236
236
|
cwd: projectRoot,
|
|
237
237
|
errorName: 'lerna publish',
|
|
238
238
|
stdio,
|
|
@@ -319,7 +319,7 @@ export function buildSuggestedInitCommand(result) {
|
|
|
319
319
|
const normalizedRegistry = result.npmRegistry || `http://${host}:${port || DEFAULT_SOURCE_REGISTRY_PORT}`;
|
|
320
320
|
const suggestedEnv = ['snapshot', sanitizeEnvSegment(result.gitSha)].filter(Boolean).join('');
|
|
321
321
|
return [
|
|
322
|
-
`nb init --
|
|
322
|
+
`nb init --env ${suggestedEnv} --yes --source npm`,
|
|
323
323
|
`--version ${result.version}`,
|
|
324
324
|
`--npm-registry=${normalizedRegistry}`,
|
|
325
325
|
].join(' ');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.7",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
@@ -143,5 +143,5 @@
|
|
|
143
143
|
"type": "git",
|
|
144
144
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
145
145
|
},
|
|
146
|
-
"gitHead": "
|
|
146
|
+
"gitHead": "e6a3fa8963a73cd9ddfc1273d71b0012483e1ad8"
|
|
147
147
|
}
|