@fecommunity/reactpress 3.0.0 → 3.0.2

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.
Files changed (62) hide show
  1. package/README.md +67 -21
  2. package/bin/reactpress.js +8 -5
  3. package/dist/index.js +0 -0
  4. package/lib/api-dev.js +4 -4
  5. package/lib/bootstrap.js +2 -2
  6. package/lib/build.js +130 -13
  7. package/lib/dev.js +2 -14
  8. package/lib/i18n/strings.js +32 -2
  9. package/lib/lifecycle.js +8 -8
  10. package/lib/paths.js +39 -19
  11. package/lib/pm2.js +2 -2
  12. package/lib/publish.js +9 -0
  13. package/lib/root.js +63 -6
  14. package/lib/spawn.js +15 -10
  15. package/lib/status.js +1 -1
  16. package/package.json +7 -4
  17. package/server/dist/app.module.js +1 -26
  18. package/server/dist/app.module.js.map +1 -1
  19. package/server/dist/modules/article/article.controller.d.ts +1 -6
  20. package/server/dist/modules/article/article.controller.js +1 -45
  21. package/server/dist/modules/article/article.controller.js.map +1 -1
  22. package/server/dist/modules/article/article.entity.d.ts +0 -1
  23. package/server/dist/modules/article/article.entity.js +0 -5
  24. package/server/dist/modules/article/article.entity.js.map +1 -1
  25. package/server/dist/modules/article/article.module.js +2 -14
  26. package/server/dist/modules/article/article.module.js.map +1 -1
  27. package/server/dist/modules/article/article.service.d.ts +1 -9
  28. package/server/dist/modules/article/article.service.js +5 -59
  29. package/server/dist/modules/article/article.service.js.map +1 -1
  30. package/server/dist/modules/auth/auth.service.js +3 -4
  31. package/server/dist/modules/auth/auth.service.js.map +1 -1
  32. package/server/dist/modules/auth/jwt-auth.guard.js +1 -2
  33. package/server/dist/modules/auth/jwt-auth.guard.js.map +1 -1
  34. package/server/dist/modules/auth/jwt.strategy.js +1 -2
  35. package/server/dist/modules/auth/jwt.strategy.js.map +1 -1
  36. package/server/dist/modules/category/category.service.js +2 -3
  37. package/server/dist/modules/category/category.service.js.map +1 -1
  38. package/server/dist/modules/comment/comment.module.js +1 -10
  39. package/server/dist/modules/comment/comment.module.js.map +1 -1
  40. package/server/dist/modules/comment/comment.service.d.ts +1 -3
  41. package/server/dist/modules/comment/comment.service.js +5 -15
  42. package/server/dist/modules/comment/comment.service.js.map +1 -1
  43. package/server/dist/modules/knowledge/knowledge.service.js +2 -3
  44. package/server/dist/modules/knowledge/knowledge.service.js.map +1 -1
  45. package/server/dist/modules/page/page.service.js +1 -2
  46. package/server/dist/modules/page/page.service.js.map +1 -1
  47. package/server/dist/modules/smtp/smtp.service.js +1 -2
  48. package/server/dist/modules/smtp/smtp.service.js.map +1 -1
  49. package/server/dist/modules/tag/tag.service.js +2 -3
  50. package/server/dist/modules/tag/tag.service.js.map +1 -1
  51. package/server/dist/modules/user/user.controller.js +3 -4
  52. package/server/dist/modules/user/user.controller.js.map +1 -1
  53. package/server/dist/modules/user/user.service.js +9 -10
  54. package/server/dist/modules/user/user.service.js.map +1 -1
  55. package/server/dist/starter.js +1 -1
  56. package/server/dist/tsconfig.build.tsbuildinfo +11335 -11537
  57. package/server/dist/utils/oss.util.js +1 -2
  58. package/server/dist/utils/oss.util.js.map +1 -1
  59. package/templates/env.default +1 -1
  60. package/templates/package.json +3 -2
  61. package/ui/banner.js +2 -2
  62. package/ui/interactive.js +20 -4
package/README.md CHANGED
@@ -1,47 +1,93 @@
1
- # @fecommunity/reactpress-cli
1
+ # @fecommunity/reactpress
2
2
 
3
- 零配置一键初始化与管理 ReactPress CMS & 博客服务器。内置 NestJS 服务端,无需单独克隆 [fecommunity/reactpress](https://github.com/fecommunity/reactpress)。
3
+ ReactPress 3.x 主包:一条命令完成站点初始化、本地开发、构建与生产部署。内置 NestJS API(站点项目无 `server/` 时自动使用 bundle),Monorepo 贡献者可使用仓库内 `server/` 源码联调。
4
4
 
5
- 完整文档与贡献指南见:[github.com/fecommunity/reactpress-cli](https://github.com/fecommunity/reactpress-cli)
5
+ [![npm version](https://img.shields.io/npm/v/@fecommunity/reactpress.svg)](https://www.npmjs.com/package/@fecommunity/reactpress)
6
+ [![License: MIT](https://img.shields.io/npm/l/@fecommunity/reactpress.svg)](https://github.com/fecommunity/reactpress/blob/master/cli/LICENSE)
7
+
8
+ 完整文档:[github.com/fecommunity/reactpress](https://github.com/fecommunity/reactpress) · 中文说明见仓库 [README-zh_CN.md](https://github.com/fecommunity/reactpress/blob/master/README-zh_CN.md)
6
9
 
7
10
  ## 安装
8
11
 
9
12
  ```bash
10
- npm install -g @fecommunity/reactpress-cli
13
+ npm install -g @fecommunity/reactpress@3
11
14
  ```
12
15
 
13
- 全局命令为 `reactpress-cli`(与 npm 包名无关)。
16
+ 全局命令为 **`reactpress`**(推荐)。
14
17
 
15
- > npm 上的无作用域包名 `reactpress-cli` 已被占用,本包发布为 `@fecommunity/reactpress-cli`。
18
+ > **兼容**:仍提供 `reactpress-cli` 别名,会映射到 `reactpress server *` 并打印弃用提示,**3.1 将移除**。旧包名 `@fecommunity/reactpress-cli` 请迁移到本包。
16
19
 
17
20
  ## 快速开始
18
21
 
22
+ ### 新建站点(零配置)
23
+
19
24
  ```bash
20
25
  mkdir my-blog && cd my-blog
21
- reactpress-cli init
22
- reactpress-cli start
26
+ reactpress init
27
+ reactpress dev
23
28
  ```
24
29
 
25
- 浏览器访问 `http://localhost:3002`(API 文档:`/api`)。
30
+ | 服务 | 默认地址 |
31
+ |------|----------|
32
+ | 前台 | http://localhost:3001 |
33
+ | 后台 | http://localhost:3001/admin |
34
+ | API / Swagger | http://localhost:3002/api |
35
+
36
+ ### 在本仓库(Monorepo)开发
37
+
38
+ ```bash
39
+ git clone https://github.com/fecommunity/reactpress.git
40
+ cd reactpress
41
+ pnpm install
42
+ pnpm init # 生成 .env、.reactpress/config.json
43
+ pnpm dev # 等价于 reactpress dev
44
+ ```
26
45
 
27
46
  ## 常用命令
28
47
 
29
48
  | 命令 | 说明 |
30
49
  |------|------|
31
- | `reactpress-cli init [dir]` | 初始化项目 |
32
- | `reactpress-cli start` | 启动服务(自动准备数据库) |
33
- | `reactpress-cli stop` | 停止服务 |
34
- | `reactpress-cli restart` | 重启服务 |
35
- | `reactpress-cli status` | 查看状态 |
36
- | `reactpress-cli config [key] [value]` | 查看/修改配置 |
37
- | `reactpress-cli config server.port 3003 --apply` | 改端口并重启 |
50
+ | `reactpress` | 交互式菜单 |
51
+ | `reactpress init [dir]` | 初始化项目(`-f` 强制) |
52
+ | `reactpress dev` | 环境检查 → 构建 toolkit → API + 前台 |
53
+ | `reactpress dev --api-only` | 仅 API |
54
+ | `reactpress dev --client-only` | 仅前台(需 API 已运行) |
55
+ | `reactpress build -t <target>` | 构建:`toolkit` \| `server` \| `client` \| `docs` \| `all` |
56
+ | `reactpress server start` | 后台启动 API(`--bg` 不等待健康检查) |
57
+ | `reactpress server stop` / `restart` / `status` | API 生命周期 |
58
+ | `reactpress start` | 生产:API + 前台 |
59
+ | `reactpress status` | 统一状态(配置、端口、健康检查) |
60
+ | `reactpress doctor` | 环境诊断 |
61
+ | `reactpress docker up` / `down` / `start` / `status` / `logs` | Docker MySQL 与编排 |
62
+ | `reactpress db backup` | 数据库备份(`mysqldump`) |
63
+ | `reactpress publish` | Monorepo 发包向导(维护者) |
64
+
65
+ 站点级配置(`reactpress-cli` 时代)仍通过项目内 **`pnpm exec reactpress-cli config`** 或 `.reactpress/config.json` 管理;3.x 主路径为 `reactpress` + 根目录 `.env`。
66
+
67
+ ## 环境要求
68
+
69
+ - **Node.js** ≥ 18
70
+ - **pnpm** ≥ 8(Monorepo / `pnpm dev`)
71
+ - **MySQL**:Docker(`reactpress docker up`)或 `.env` / `.reactpress/config.json` 中的外部库
72
+
73
+ ## 故障排查
74
+
75
+ | 现象 | 处理 |
76
+ |------|------|
77
+ | `Missing script: build:toolkit` | 勿将 `REACTPRESS_ORIGINAL_CWD` 指到全局 npm 安装目录;在项目根执行,或 `unset REACTPRESS_ORIGINAL_CWD` 后重试 |
78
+ | 误用 bundle API、未走 Monorepo `server/` | 确保在含 `pnpm-workspace.yaml` 的仓库根目录运行;升级至最新 `@fecommunity/reactpress` |
79
+ | 端口占用 | `reactpress doctor` 或修改 `.env` 中 `SERVER_PORT` / `CLIENT_PORT` |
80
+
81
+ ## 相关包
38
82
 
39
- ## 要求
83
+ | 包 | 用途 |
84
+ |----|------|
85
+ | `@fecommunity/reactpress-client` | 仅部署 Next.js 前台 |
86
+ | `@fecommunity/reactpress-toolkit` | OpenAPI 生成的 TS SDK |
87
+ | `@fecommunity/reactpress-template-*` | 项目模板 |
40
88
 
41
- - Node.js 18+
42
- - macOS / Linux / Windows
43
- - 默认使用 Docker 运行嵌入式 MySQL;也可在 `.reactpress/config.json` 中配置外部数据库
89
+ `@fecommunity/reactpress-server`、独立安装 `@fecommunity/reactpress-cli` 已弃用,请使用本包。
44
90
 
45
91
  ## 许可证
46
92
 
47
- MIT © FECommunity
93
+ MIT © [FECommunity](https://github.com/fecommunity)
package/bin/reactpress.js CHANGED
@@ -8,7 +8,8 @@
8
8
  const { Command } = require('commander');
9
9
  const path = require('path');
10
10
  const chalk = require('chalk');
11
- const { ensureOriginalCwd, getMonorepoRoot } = require('../lib/root');
11
+ const { ensureOriginalCwd } = require('../lib/root');
12
+ const { getCliPackageRoot } = require('../lib/paths');
12
13
  const { ensureProjectEnvironment, initMonorepoProject } = require('../lib/bootstrap');
13
14
  const { runDev } = require('../lib/dev');
14
15
  const { runApiDev } = require('../lib/api-dev');
@@ -24,7 +25,7 @@ const { getClientBin } = require('../lib/paths');
24
25
  const { runInteractiveLoop } = require('../ui/interactive');
25
26
  const { t } = require('../lib/i18n');
26
27
 
27
- const rootPkg = require(path.join(getMonorepoRoot(), 'package.json'));
28
+ const rootPkg = require(path.join(getCliPackageRoot(), 'package.json'));
28
29
 
29
30
  const program = new Command();
30
31
 
@@ -62,7 +63,7 @@ program
62
63
  const projectRoot = ensureOriginalCwd();
63
64
  try {
64
65
  if (options.clientOnly) {
65
- await runNodeScript(getClientBin(), [], { cwd: projectRoot });
66
+ await runNodeScript(getClientBin(projectRoot), [], { cwd: projectRoot });
66
67
  return;
67
68
  }
68
69
  if (options.apiOnly) {
@@ -121,13 +122,14 @@ clientCmd
121
122
  .option('--pm2', t('cli.client.start.pm2'))
122
123
  .action(async (options) => {
123
124
  const args = options.pm2 ? ['--pm2'] : [];
124
- await runNodeScript(getClientBin(), args, { cwd: ensureOriginalCwd() });
125
+ const projectRoot = ensureOriginalCwd();
126
+ await runNodeScript(getClientBin(projectRoot), args, { cwd: projectRoot });
125
127
  });
126
128
 
127
129
  program
128
130
  .command('build')
129
131
  .description(t('cli.build.description'))
130
- .option('-t, --target <target>', 'toolkit | server | client | docs | all', 'all')
132
+ .option('-t, --target <target>', t('cli.build.target'), 'all')
131
133
  .action(async (options) => {
132
134
  try {
133
135
  await runBuild(options.target, ensureOriginalCwd());
@@ -254,6 +256,7 @@ program.on('--help', () => {
254
256
  console.log(t('cli.help.status'));
255
257
  console.log(t('cli.help.doctor'));
256
258
  console.log(t('cli.help.docker'));
259
+ console.log(t('cli.help.build'));
257
260
  console.log(t('cli.help.publish'));
258
261
  console.log('');
259
262
  });
package/dist/index.js CHANGED
File without changes
package/lib/api-dev.js CHANGED
@@ -2,7 +2,7 @@ const { spawn, spawnSync } = require('child_process');
2
2
  const path = require('path');
3
3
  const { ensureProjectEnvironment } = require('./bootstrap');
4
4
  const { isUsingMonorepoServer } = require('./paths');
5
- const { getMonorepoRoot } = require('./root');
5
+ const { ensureOriginalCwd } = require('./root');
6
6
  const { t } = require('./i18n');
7
7
 
8
8
  let apiChild;
@@ -11,7 +11,7 @@ function stopApiDev(projectRoot) {
11
11
  if (apiChild && !apiChild.killed) {
12
12
  apiChild.kill('SIGTERM');
13
13
  }
14
- if (!isUsingMonorepoServer()) {
14
+ if (!isUsingMonorepoServer(projectRoot)) {
15
15
  spawnSync('pnpm', ['exec', 'reactpress-cli', 'stop'], {
16
16
  cwd: projectRoot,
17
17
  stdio: 'inherit',
@@ -20,7 +20,7 @@ function stopApiDev(projectRoot) {
20
20
  }
21
21
 
22
22
  function startApiDev(projectRoot) {
23
- if (isUsingMonorepoServer()) {
23
+ if (isUsingMonorepoServer(projectRoot)) {
24
24
  console.log(t('apiDev.modeServer'));
25
25
  apiChild = spawn('pnpm', ['run', '--dir', './server', 'dev'], {
26
26
  cwd: projectRoot,
@@ -54,7 +54,7 @@ function startApiDev(projectRoot) {
54
54
  }
55
55
  }
56
56
 
57
- async function runApiDev(projectRoot = process.env.REACTPRESS_ORIGINAL_CWD || getMonorepoRoot()) {
57
+ async function runApiDev(projectRoot = ensureOriginalCwd()) {
58
58
  try {
59
59
  await ensureProjectEnvironment(projectRoot);
60
60
  } catch (err) {
package/lib/bootstrap.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { pathToFileURL } = require('url');
4
- const { getMonorepoRoot, isMonorepoCheckout } = require('./root');
4
+ const { ensureOriginalCwd, isMonorepoCheckout } = require('./root');
5
5
  const { getCliPackageRoot } = require('./paths');
6
6
  const { t } = require('./i18n');
7
7
 
@@ -68,7 +68,7 @@ async function initMonorepoProject(projectRoot, { force = false } = {}) {
68
68
  };
69
69
  }
70
70
 
71
- async function ensureProjectEnvironment(projectRoot = getMonorepoRoot()) {
71
+ async function ensureProjectEnvironment(projectRoot = ensureOriginalCwd()) {
72
72
  const root = path.resolve(projectRoot);
73
73
  const { setProjectCwd } = await importCliModule('utils/cli-context.js');
74
74
  setProjectCwd(root);
package/lib/build.js CHANGED
@@ -1,29 +1,146 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
1
4
  const { runSync } = require('./spawn');
2
5
  const { ensureOriginalCwd } = require('./root');
3
6
  const { t } = require('./i18n');
4
7
 
5
- const TARGETS = {
6
- toolkit: ['build:toolkit'],
7
- server: ['build:server'],
8
- client: ['build:client'],
9
- docs: ['build:docs'],
10
- all: ['build'],
8
+ const FORBIDDEN_SCRIPTS = new Set(['build']);
9
+
10
+ /** @type {Record<string, { script: string, labelKey: string }[]>} */
11
+ const BUILD_STEPS = {
12
+ toolkit: [{ script: 'build:toolkit', labelKey: 'build.label.toolkit' }],
13
+ server: [{ script: 'build:server', labelKey: 'build.label.server' }],
14
+ client: [{ script: 'build:client', labelKey: 'build.label.client' }],
15
+ docs: [{ script: 'build:docs', labelKey: 'build.label.docs' }],
16
+ all: [
17
+ { script: 'build:toolkit', labelKey: 'build.label.toolkit' },
18
+ { script: 'build:server', labelKey: 'build.label.server' },
19
+ { script: 'build:client', labelKey: 'build.label.client' },
20
+ ],
11
21
  };
12
22
 
23
+ const TARGETS = Object.keys(BUILD_STEPS);
24
+
25
+ const buildChildEnv = { REACTPRESS_BUILD_ACTIVE: '1' };
26
+
27
+ function readPackageScripts(packageJsonPath) {
28
+ try {
29
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
30
+ return pkg.scripts || {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ /** Prefer workspace package scripts over root package.json aliases. */
37
+ function resolveBuildInvocation(script, projectRoot) {
38
+ const root = path.resolve(projectRoot);
39
+
40
+ if (script === 'build:toolkit') {
41
+ const toolkitDir = path.join(root, 'toolkit');
42
+ if (fs.existsSync(path.join(toolkitDir, 'package.json'))) {
43
+ return { command: 'pnpm', args: ['run', 'build'], cwd: toolkitDir };
44
+ }
45
+ const rootScripts = readPackageScripts(path.join(root, 'package.json'));
46
+ if (rootScripts['build:toolkit']) {
47
+ return { command: 'pnpm', args: ['run', 'build:toolkit'], cwd: root };
48
+ }
49
+ return null;
50
+ }
51
+
52
+ if (script === 'build:server') {
53
+ const serverDir = path.join(root, 'server');
54
+ if (fs.existsSync(path.join(serverDir, 'package.json'))) {
55
+ return { command: 'pnpm', args: ['run', 'build'], cwd: serverDir };
56
+ }
57
+ }
58
+
59
+ if (script === 'build:client') {
60
+ const clientDir = path.join(root, 'client');
61
+ if (fs.existsSync(path.join(clientDir, 'package.json'))) {
62
+ return { command: 'pnpm', args: ['run', 'build'], cwd: clientDir };
63
+ }
64
+ }
65
+
66
+ if (script === 'build:docs') {
67
+ const docsDir = path.join(root, 'docs');
68
+ if (fs.existsSync(path.join(docsDir, 'package.json'))) {
69
+ return { command: 'pnpm', args: ['run', 'build'], cwd: docsDir };
70
+ }
71
+ }
72
+
73
+ const rootScripts = readPackageScripts(path.join(root, 'package.json'));
74
+ if (rootScripts[script]) {
75
+ return { command: 'pnpm', args: ['run', script], cwd: root };
76
+ }
77
+
78
+ return null;
79
+ }
80
+
13
81
  async function runBuild(target = 'all', projectRoot = ensureOriginalCwd()) {
14
- const script = TARGETS[target];
15
- if (!script) {
82
+ if (process.env.REACTPRESS_BUILD_ACTIVE === '1') {
83
+ throw new Error(t('build.recursive'));
84
+ }
85
+
86
+ const steps = BUILD_STEPS[target];
87
+ if (!steps) {
16
88
  throw new Error(
17
89
  t('build.unknownTarget', {
18
90
  target,
19
- available: Object.keys(TARGETS).join(', '),
91
+ available: TARGETS.join(', '),
20
92
  })
21
93
  );
22
94
  }
23
- for (const name of script) {
24
- console.log(t('build.running', { name }));
25
- runSync('pnpm', ['run', name], { cwd: projectRoot });
95
+
96
+ const total = steps.length;
97
+ const buildStarted = Date.now();
98
+
99
+ if (total > 1) {
100
+ console.log(t('build.plan', { total }));
101
+ }
102
+
103
+ for (let i = 0; i < steps.length; i++) {
104
+ const { script, labelKey } = steps[i];
105
+ if (FORBIDDEN_SCRIPTS.has(script)) {
106
+ throw new Error(t('build.forbiddenScript', { script }));
107
+ }
108
+
109
+ const current = i + 1;
110
+ const label = t(labelKey);
111
+ const stepStarted = Date.now();
112
+
113
+ console.log(t('build.step', { current, total, label }));
114
+
115
+ const invocation = resolveBuildInvocation(script, projectRoot);
116
+ if (!invocation) {
117
+ console.log(chalk.yellow(t('build.stepSkipped', { label })));
118
+ continue;
119
+ }
120
+
121
+ try {
122
+ runSync(invocation.command, invocation.args, {
123
+ cwd: invocation.cwd,
124
+ env: buildChildEnv,
125
+ });
126
+ } catch (err) {
127
+ console.error(chalk.red(t('build.stepFailed', { current, total, label })));
128
+ throw err;
129
+ }
130
+
131
+ const seconds = ((Date.now() - stepStarted) / 1000).toFixed(1);
132
+ console.log(chalk.green(t('build.stepDone', { current, total, label, seconds })));
133
+ }
134
+
135
+ if (total > 1) {
136
+ const totalSeconds = ((Date.now() - buildStarted) / 1000).toFixed(1);
137
+ console.log(chalk.green(t('build.done', { seconds: totalSeconds })));
26
138
  }
27
139
  }
28
140
 
29
- module.exports = { runBuild, TARGETS };
141
+ module.exports = {
142
+ runBuild,
143
+ TARGETS,
144
+ BUILD_STEPS,
145
+ resolveBuildInvocation,
146
+ };
package/lib/dev.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { spawn } = require('child_process');
2
2
  const path = require('path');
3
+ const { runBuild } = require('./build');
3
4
  const { ensureProjectEnvironment } = require('./bootstrap');
4
5
  const { loadServerSiteUrl, loadClientSiteUrl, waitForHttp } = require('./http');
5
6
  const { printDevReadyBanner } = require('./dev-banner');
@@ -30,20 +31,7 @@ function shutdown(signal = 'SIGINT') {
30
31
  }
31
32
 
32
33
  async function buildToolkit(projectRoot) {
33
- return new Promise((resolve, reject) => {
34
- const build = spawn('pnpm', ['build:toolkit'], {
35
- stdio: 'inherit',
36
- shell: true,
37
- cwd: projectRoot,
38
- });
39
- build.on('close', (code) => {
40
- if (code !== 0) {
41
- reject(new Error(t('dev.toolkitFailed', { code })));
42
- return;
43
- }
44
- resolve();
45
- });
46
- });
34
+ await runBuild('toolkit', projectRoot);
47
35
  }
48
36
 
49
37
  async function startDevStack(projectRoot) {
@@ -43,7 +43,9 @@ const STRINGS = {
43
43
  'cli.help.status': ' reactpress status Combined status',
44
44
  'cli.help.doctor': ' reactpress doctor Environment diagnostics',
45
45
  'cli.help.docker': ' reactpress docker start Docker + full stack',
46
+ 'cli.help.build': ' reactpress build -t client Build one target (toolkit|server|client|docs|all)',
46
47
  'cli.help.publish': ' reactpress publish Publish npm packages',
48
+ 'cli.build.target': 'Build target: toolkit | server | client | docs | all',
47
49
  'banner.subtitle': ' · Full-stack publishing CLI ',
48
50
  'menu.dev': 'Zero-config dev (env + DB + API + frontend)',
49
51
  'menu.init': 'Initialize project (.reactpress + .env + database)',
@@ -55,6 +57,8 @@ const STRINGS = {
55
57
  'menu.serverStop': 'Stop API',
56
58
  'menu.serverRestart': 'Restart API',
57
59
  'menu.build': 'Build (toolkit → server → client)',
60
+ 'menu.buildTarget': 'What do you want to build?',
61
+ 'menu.buildAll': 'All (toolkit → server → client)',
58
62
  'menu.dockerStart': 'Docker dev (DB + nginx + full stack)',
59
63
  'menu.dockerUp': 'Docker: database only',
60
64
  'menu.dockerStop': 'Stop Docker services',
@@ -193,7 +197,18 @@ const STRINGS = {
193
197
  'apiDev.ctrlCHint': '[reactpress] Press Ctrl+C to stop API.',
194
198
  'apiDev.stopHint': '[reactpress] Stop separately: reactpress server stop',
195
199
  'build.unknownTarget': 'Unknown build target: {target}. Available: {available}',
196
- 'build.running': '[reactpress] Building: {name}',
200
+ 'build.recursive': 'Build recursion detected (pnpm run build must not call itself). Use build:toolkit, build:server, or build:client.',
201
+ 'build.forbiddenScript': 'Invalid build script "{script}"; use granular scripts like build:toolkit.',
202
+ 'build.stepFailed': '[reactpress] [{current}/{total}] {label} failed',
203
+ 'build.plan': '[reactpress] Production build — {total} step(s): toolkit → server → client',
204
+ 'build.step': '[reactpress] [{current}/{total}] {label}…',
205
+ 'build.stepDone': '[reactpress] [{current}/{total}] {label} ✓ ({seconds}s)',
206
+ 'build.stepSkipped': '[reactpress] Skipped {label} (not part of this project layout)',
207
+ 'build.done': '[reactpress] Build finished in {seconds}s',
208
+ 'build.label.toolkit': 'Toolkit',
209
+ 'build.label.server': 'API (server)',
210
+ 'build.label.client': 'Frontend (client)',
211
+ 'build.label.docs': 'Documentation (docs)',
197
212
  'pm2.startFailed': '[reactpress] PM2 failed to start API:',
198
213
  'pm2.exitCode': 'PM2 exited with code {code}',
199
214
  'spawn.commandFailed': 'Command failed ({command}): exit code {code}',
@@ -320,7 +335,9 @@ const STRINGS = {
320
335
  'cli.help.status': ' reactpress status 综合状态',
321
336
  'cli.help.doctor': ' reactpress doctor 环境诊断',
322
337
  'cli.help.docker': ' reactpress docker start Docker + 全栈',
338
+ 'cli.help.build': ' reactpress build -t client 构建指定目标 (toolkit|server|client|docs|all)',
323
339
  'cli.help.publish': ' reactpress publish 发布 npm 包',
340
+ 'cli.build.target': '构建目标: toolkit | server | client | docs | all',
324
341
  'banner.subtitle': ' · 全栈发布平台 CLI ',
325
342
  'menu.dev': '零配置开发 (env + DB + API + 前端)',
326
343
  'menu.init': '初始化项目 (.reactpress + .env + 数据库)',
@@ -332,6 +349,8 @@ const STRINGS = {
332
349
  'menu.serverStop': '停止 API',
333
350
  'menu.serverRestart': '重启 API',
334
351
  'menu.build': '构建 (toolkit → server → client)',
352
+ 'menu.buildTarget': '选择要构建的目标',
353
+ 'menu.buildAll': '全部 (toolkit → server → client)',
335
354
  'menu.dockerStart': 'Docker 开发环境 (DB + nginx + 全栈)',
336
355
  'menu.dockerUp': 'Docker 仅启动数据库',
337
356
  'menu.dockerStop': '停止 Docker 服务',
@@ -470,7 +489,18 @@ const STRINGS = {
470
489
  'apiDev.ctrlCHint': '[reactpress] 按 Ctrl+C 停止 API。',
471
490
  'apiDev.stopHint': '[reactpress] 单独停止: reactpress server stop',
472
491
  'build.unknownTarget': '未知构建目标: {target},可选: {available}',
473
- 'build.running': '[reactpress] 构建: {name}',
492
+ 'build.recursive': '检测到构建递归(pnpm run build 不能再次调用自身)。请使用 build:toolkit、build:server 或 build:client。',
493
+ 'build.forbiddenScript': '无效的构建脚本 "{script}",请使用 build:toolkit 等细分脚本。',
494
+ 'build.stepFailed': '[reactpress] [{current}/{total}] {label} 失败',
495
+ 'build.plan': '[reactpress] 生产构建 — 共 {total} 步:toolkit → server → client',
496
+ 'build.step': '[reactpress] [{current}/{total}] {label}…',
497
+ 'build.stepDone': '[reactpress] [{current}/{total}] {label} ✓ ({seconds}s)',
498
+ 'build.stepSkipped': '[reactpress] 已跳过 {label}(当前项目无对应源码包)',
499
+ 'build.done': '[reactpress] 构建完成,耗时 {seconds}s',
500
+ 'build.label.toolkit': 'Toolkit',
501
+ 'build.label.server': 'API (server)',
502
+ 'build.label.client': '前端 (client)',
503
+ 'build.label.docs': '文档 (docs)',
474
504
  'pm2.startFailed': '[reactpress] PM2 启动 API 失败:',
475
505
  'pm2.exitCode': 'PM2 退出码 {code}',
476
506
  'spawn.commandFailed': '命令失败 ({command}): 退出码 {code}',
package/lib/lifecycle.js CHANGED
@@ -8,7 +8,7 @@ const {
8
8
  getPidFile,
9
9
  } = require('./paths');
10
10
  const { readPid, isProcessRunning, clearPidFile, writePid } = require('./process');
11
- const { getMonorepoRoot } = require('./root');
11
+ const { ensureOriginalCwd } = require('./root');
12
12
  const { t } = require('./i18n');
13
13
 
14
14
  async function ensureConfig(projectRoot) {
@@ -46,7 +46,7 @@ async function startApi(projectRoot, { wait = true } = {}) {
46
46
  }
47
47
  clearPidFile(projectRoot);
48
48
 
49
- if (!isUsingMonorepoServer()) {
49
+ if (!isUsingMonorepoServer(projectRoot)) {
50
50
  console.warn(t('lifecycle.noServerSrc'));
51
51
  const start = spawnSync('pnpm', ['exec', 'reactpress-cli', 'start'], {
52
52
  cwd: projectRoot,
@@ -56,8 +56,8 @@ async function startApi(projectRoot, { wait = true } = {}) {
56
56
  }
57
57
 
58
58
  console.log(t('lifecycle.startingLocalApi'));
59
- const child = spawn(process.execPath, [getServerBin()], {
60
- cwd: getServerDir(),
59
+ const child = spawn(process.execPath, [getServerBin(projectRoot)], {
60
+ cwd: getServerDir(projectRoot),
61
61
  detached: true,
62
62
  stdio: 'ignore',
63
63
  env: {
@@ -90,7 +90,7 @@ async function statusApi(projectRoot) {
90
90
  const { isHttpResponding } = require('./http');
91
91
  const httpOk = await isHttpResponding(serverUrl);
92
92
 
93
- const source = isUsingMonorepoServer()
93
+ const source = isUsingMonorepoServer(projectRoot)
94
94
  ? t('lifecycle.source.monorepo')
95
95
  : t('lifecycle.source.bundle');
96
96
 
@@ -119,7 +119,7 @@ async function statusApi(projectRoot) {
119
119
  );
120
120
  }
121
121
 
122
- async function runLifecycleCommand(command, projectRoot = process.env.REACTPRESS_ORIGINAL_CWD || getMonorepoRoot()) {
122
+ async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) {
123
123
  switch (command) {
124
124
  case 'start':
125
125
  return startApi(projectRoot, { wait: true });
@@ -127,7 +127,7 @@ async function runLifecycleCommand(command, projectRoot = process.env.REACTPRESS
127
127
  return startApi(projectRoot, { wait: false });
128
128
  case 'stop':
129
129
  stopApi(projectRoot);
130
- if (!isUsingMonorepoServer()) {
130
+ if (!isUsingMonorepoServer(projectRoot)) {
131
131
  spawnSync('pnpm', ['exec', 'reactpress-cli', 'stop'], {
132
132
  cwd: projectRoot,
133
133
  stdio: 'inherit',
@@ -136,7 +136,7 @@ async function runLifecycleCommand(command, projectRoot = process.env.REACTPRESS
136
136
  return 0;
137
137
  case 'restart':
138
138
  stopApi(projectRoot);
139
- if (!isUsingMonorepoServer()) {
139
+ if (!isUsingMonorepoServer(projectRoot)) {
140
140
  spawnSync('pnpm', ['exec', 'reactpress-cli', 'restart'], {
141
141
  cwd: projectRoot,
142
142
  stdio: 'inherit',
package/lib/paths.js CHANGED
@@ -1,13 +1,19 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { getMonorepoRoot } = require('./root');
3
+ const { ensureOriginalCwd, getMonorepoRoot } = require('./root');
4
4
 
5
- function getMonorepoServerDir() {
6
- return path.join(getMonorepoRoot(), 'server');
5
+ function resolveProjectRoot(projectRoot) {
6
+ return path.resolve(projectRoot || ensureOriginalCwd());
7
7
  }
8
8
 
9
- function hasMonorepoServerSource() {
10
- return fs.existsSync(path.join(getMonorepoServerDir(), 'src', 'main.ts'));
9
+ function getMonorepoServerDir(projectRoot) {
10
+ return path.join(resolveProjectRoot(projectRoot), 'server');
11
+ }
12
+
13
+ function hasMonorepoServerSource(projectRoot) {
14
+ return fs.existsSync(
15
+ path.join(getMonorepoServerDir(projectRoot), 'src', 'main.ts')
16
+ );
11
17
  }
12
18
 
13
19
  function getCliPackageRoot() {
@@ -28,39 +34,53 @@ function getBundledServerDir() {
28
34
  return path.join(getCliPackageRoot(), 'server');
29
35
  }
30
36
 
31
- function getServerDir() {
32
- if (hasMonorepoServerSource()) {
33
- return getMonorepoServerDir();
37
+ function getServerDir(projectRoot) {
38
+ if (hasMonorepoServerSource(projectRoot)) {
39
+ return getMonorepoServerDir(projectRoot);
34
40
  }
35
41
  return getBundledServerDir();
36
42
  }
37
43
 
38
- function getServerBin() {
39
- return path.join(getServerDir(), 'bin', 'reactpress-server.js');
44
+ function getServerBin(projectRoot) {
45
+ return path.join(getServerDir(projectRoot), 'bin', 'reactpress-server.js');
40
46
  }
41
47
 
42
- function getSwaggerPath() {
43
- return path.join(getServerDir(), 'public', 'swagger.json');
48
+ function getSwaggerPath(projectRoot) {
49
+ return path.join(getServerDir(projectRoot), 'public', 'swagger.json');
44
50
  }
45
51
 
46
- function getServerMain() {
47
- return path.join(getServerDir(), 'dist', 'main.js');
52
+ function getServerMain(projectRoot) {
53
+ return path.join(getServerDir(projectRoot), 'dist', 'main.js');
48
54
  }
49
55
 
50
- function isUsingMonorepoServer() {
51
- return hasMonorepoServerSource();
56
+ function isUsingMonorepoServer(projectRoot) {
57
+ return hasMonorepoServerSource(projectRoot);
52
58
  }
53
59
 
54
- function getClientBin() {
55
- return path.join(getMonorepoRoot(), 'client', 'bin', 'reactpress-client.js');
60
+ function getClientBin(projectRoot) {
61
+ const binPath = path.join(
62
+ resolveProjectRoot(projectRoot),
63
+ 'client',
64
+ 'bin',
65
+ 'reactpress-client.js'
66
+ );
67
+ if (!fs.existsSync(binPath)) {
68
+ const err = new Error(
69
+ `Client entry not found: ${binPath}. Run from a ReactPress monorepo root or use reactpress-cli for site-only projects.`
70
+ );
71
+ err.code = 'REACTPRESS_CLIENT_NOT_FOUND';
72
+ throw err;
73
+ }
74
+ return binPath;
56
75
  }
57
76
 
58
77
  function getPidFile(projectRoot) {
59
- return path.join(projectRoot, '.reactpress', 'server.pid');
78
+ return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'server.pid');
60
79
  }
61
80
 
62
81
  module.exports = {
63
82
  getMonorepoRoot,
83
+ resolveProjectRoot,
64
84
  getMonorepoServerDir,
65
85
  hasMonorepoServerSource,
66
86
  isUsingMonorepoServer,
package/lib/pm2.js CHANGED
@@ -5,9 +5,9 @@ const { t } = require('./i18n');
5
5
 
6
6
  function startApiWithPm2(projectRoot = ensureOriginalCwd()) {
7
7
  return new Promise((resolve, reject) => {
8
- const child = spawn(process.execPath, [getServerBin(), '--pm2'], {
8
+ const child = spawn(process.execPath, [getServerBin(projectRoot), '--pm2'], {
9
9
  stdio: 'inherit',
10
- cwd: getServerDir(),
10
+ cwd: getServerDir(projectRoot),
11
11
  env: {
12
12
  ...process.env,
13
13
  REACTPRESS_ORIGINAL_CWD: projectRoot,