@fecommunity/reactpress 4.0.0-beta.13 → 4.0.0-beta.15
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/README.md +49 -11
- package/out/bin/reactpress-theme-client.js +94 -26
- package/out/bin/reactpress.js +36 -24
- package/out/core/services/local-site.js +3 -2
- package/out/lib/client-lifecycle.js +88 -0
- package/out/lib/i18n/strings.js +54 -12
- package/out/lib/lifecycle.js +96 -41
- package/out/lib/paths.js +4 -0
- package/out/lib/pm2-runtime.js +95 -0
- package/out/lib/process.js +25 -5
- package/out/lib/project-logs.js +1 -1
- package/out/lib/publish.js +3 -3
- package/out/lib/theme-api-proxy-patch.js +103 -0
- package/out/lib/theme-install.js +2 -0
- package/package.json +18 -3
- package/server/package-lock.json +5 -5
- package/server/package.json +18 -7
- package/toolkit/dist/theme/build/next-config.js +4 -1
- package/toolkit/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
# @fecommunity/reactpress
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Not a CMS. A publishing platform for modern developers.**
|
|
4
|
+
>
|
|
5
|
+
> WordPress-style editing · Next.js delivery · One CLI to ship.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@fecommunity/reactpress/v/beta)
|
|
8
|
+
[](https://www.npmjs.com/package/@fecommunity/reactpress)
|
|
9
|
+
[](https://www.npmjs.com/package/@fecommunity/reactpress)
|
|
10
|
+
[](https://github.com/fecommunity/reactpress/blob/master/LICENSE)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
|
|
13
|
+
The official **ReactPress CLI** — install a complete, self-hosted publishing stack in ~60 seconds. CMS API, Web Admin, and Next.js theme support ship together. No Docker, nginx, or external database required for local development.
|
|
14
|
+
|
|
15
|
+
| Layer | What you get |
|
|
16
|
+
| :---- | :----------- |
|
|
17
|
+
| **CMS API** | Headless NestJS REST — SQLite locally, MySQL in production |
|
|
18
|
+
| **Admin** | Writing console at `/admin/` — posts, pages, media, plugins, themes |
|
|
19
|
+
| **Themes** | npm-installable Next.js frontends — swap without touching content |
|
|
20
|
+
| **CLI** | `init`, `doctor`, `logs` — operate and diagnose from the terminal |
|
|
21
|
+
|
|
22
|
+
Built for frontend teams who want WordPress-grade editing without wiring five repos together.
|
|
23
|
+
|
|
24
|
+
[Documentation](https://reactpress-docs.vercel.app/) · [Live demo](https://blog.gaoredu.com) · [Theme demo](https://reactpress-theme-starter.vercel.app) · [GitHub](https://github.com/fecommunity/reactpress) · [Chinese overview](../README-zh_CN.md)
|
|
6
25
|
|
|
7
26
|
## Install
|
|
8
27
|
|
|
9
28
|
```bash
|
|
10
|
-
|
|
29
|
+
# 4.x pre-release (recommended until 4.0.0 stable)
|
|
30
|
+
npm install -g @fecommunity/reactpress@beta
|
|
31
|
+
|
|
32
|
+
# stable (3.x — legacy)
|
|
33
|
+
# npm install -g @fecommunity/reactpress
|
|
11
34
|
```
|
|
12
35
|
|
|
13
36
|
Requires Node.js 18+. On first install, `postinstall` downloads bundled server runtime dependencies (~1–2 minutes).
|
|
@@ -19,24 +42,39 @@ mkdir my-site && cd my-site
|
|
|
19
42
|
reactpress init
|
|
20
43
|
```
|
|
21
44
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
| Surface | URL |
|
|
46
|
+
| :------ | :-- |
|
|
47
|
+
| **Public site** | http://localhost:3001 |
|
|
48
|
+
| **Admin** | http://localhost:3001/admin/ (`admin` / `admin`) |
|
|
49
|
+
| **API** | http://127.0.0.1:3002/api |
|
|
50
|
+
|
|
51
|
+
Run `reactpress doctor` if something does not start correctly.
|
|
25
52
|
|
|
26
53
|
## Commands
|
|
27
54
|
|
|
28
55
|
| Command | Description |
|
|
29
|
-
|
|
30
|
-
| `reactpress init [dir]` | Initialize
|
|
56
|
+
| :------ | :---------- |
|
|
57
|
+
| `reactpress init [dir]` | Initialize a new publishing site |
|
|
31
58
|
| `reactpress doctor [dir]` | Diagnose Node.js, ports, database, and services |
|
|
32
|
-
| `reactpress
|
|
59
|
+
| `reactpress logs [dir]` | Tail API logs (error / request / response) |
|
|
60
|
+
| `reactpress stop [dir]` | Stop API and site services |
|
|
33
61
|
|
|
34
62
|
## Requirements
|
|
35
63
|
|
|
36
64
|
- Node.js 18+
|
|
37
65
|
- macOS / Linux / Windows
|
|
38
|
-
-
|
|
66
|
+
- Embedded SQLite + bundled API (no Docker or MySQL for local dev)
|
|
67
|
+
|
|
68
|
+
## Ecosystem
|
|
69
|
+
|
|
70
|
+
| Package | Role |
|
|
71
|
+
| :------ | :--- |
|
|
72
|
+
| [`@fecommunity/reactpress-toolkit`](../toolkit/) | TypeScript SDK — API clients, theme SSR, plugin hooks |
|
|
73
|
+
| [`@fecommunity/reactpress-web`](../web/) | Admin SPA — static assets and Node mount helpers |
|
|
74
|
+
| [`@fecommunity/reactpress-server`](../server/) | Standalone API (deprecated — use CLI bundled API) |
|
|
39
75
|
|
|
40
76
|
## License
|
|
41
77
|
|
|
42
|
-
MIT © FECommunity
|
|
78
|
+
MIT © [FECommunity](https://github.com/fecommunity)
|
|
79
|
+
|
|
80
|
+
<p align="center"><sub>Part of <a href="https://github.com/fecommunity/reactpress">ReactPress</a> — content owned by the system, frontend owned by developers.</sub></p>
|
|
@@ -11,11 +11,15 @@ const fs = require('fs');
|
|
|
11
11
|
const { spawn, spawnSync } = require('child_process');
|
|
12
12
|
const { hasUsableProductionBuild } = require('../lib/theme-prod');
|
|
13
13
|
const { getPm2ClientMemoryRestart, resolveBuildNodeEnv } = require('../lib/prod-memory');
|
|
14
|
+
const { resolvePm2Bin } = require('../lib/pm2-runtime');
|
|
14
15
|
const { readActiveThemeManifest } = require('../lib/theme-runtime');
|
|
15
16
|
const { ensureAdminStaticForTheme, shouldRebuildThemeAfterAdminSync, } = require('../core/services/admin-static');
|
|
17
|
+
const { patchThemeApiProxyRoute } = require('../lib/theme-api-proxy-patch');
|
|
16
18
|
const originalCwd = process.env.REACTPRESS_ORIGINAL_CWD || process.cwd();
|
|
17
19
|
const args = process.argv.slice(2);
|
|
18
20
|
const usePM2 = args.includes('--pm2');
|
|
21
|
+
const useBackground = args.includes('--bg');
|
|
22
|
+
const usePm2Background = args.includes('--pm2-bg');
|
|
19
23
|
const clientDir = process.env.REACTPRESS_THEME_DIR
|
|
20
24
|
? path.resolve(process.env.REACTPRESS_THEME_DIR)
|
|
21
25
|
: null;
|
|
@@ -33,9 +37,13 @@ function runStartCommand() {
|
|
|
33
37
|
}
|
|
34
38
|
function ensureBuilt() {
|
|
35
39
|
const adminSynced = ensureAdminStaticForTheme(originalCwd, clientDir);
|
|
40
|
+
const apiProxyPatched = patchThemeApiProxyRoute(clientDir);
|
|
36
41
|
const { activeTheme } = readActiveThemeManifest(originalCwd);
|
|
37
42
|
const themeId = process.env.REACTPRESS_THEME_ID || activeTheme || path.basename(clientDir);
|
|
38
|
-
const needsRebuild = adminSynced ||
|
|
43
|
+
const needsRebuild = adminSynced ||
|
|
44
|
+
apiProxyPatched ||
|
|
45
|
+
shouldRebuildThemeAfterAdminSync(clientDir) ||
|
|
46
|
+
!hasUsableProductionBuild(clientDir, themeId);
|
|
39
47
|
if (!needsRebuild)
|
|
40
48
|
return;
|
|
41
49
|
console.log('[ReactPress Client] Client not built yet. Building…');
|
|
@@ -47,30 +55,9 @@ function ensureBuilt() {
|
|
|
47
55
|
if (build.status !== 0)
|
|
48
56
|
process.exit(build.status || 1);
|
|
49
57
|
}
|
|
50
|
-
function
|
|
51
|
-
ensureBuilt();
|
|
52
|
-
const [cmd, cmdArgs] = runStartCommand();
|
|
53
|
-
const visitorPort = process.env.CLIENT_PORT || process.env.PORT || '3001';
|
|
54
|
-
const apiPort = process.env.SERVER_PORT || '3002';
|
|
55
|
-
const nginxEntry = (process.env.REACTPRESS_NGINX_ENTRY_URL || process.env.NGINX_ENTRY_URL || '')
|
|
56
|
-
.replace(/\/$/, '');
|
|
57
|
-
const pm2Env = {
|
|
58
|
-
...process.env,
|
|
59
|
-
NODE_ENV: 'production',
|
|
60
|
-
PORT: String(visitorPort),
|
|
61
|
-
CLIENT_PORT: String(visitorPort),
|
|
62
|
-
REACTPRESS_ORIGINAL_CWD: originalCwd,
|
|
63
|
-
REACTPRESS_THEME_DIR: clientDir,
|
|
64
|
-
REACTPRESS_API_URL: process.env.REACTPRESS_API_URL || `http://127.0.0.1:${apiPort}/api`,
|
|
65
|
-
SERVER_API_URL: process.env.SERVER_API_URL || `http://127.0.0.1:${apiPort}/api`,
|
|
66
|
-
NEXT_PUBLIC_REACTPRESS_API_URL: process.env.NEXT_PUBLIC_REACTPRESS_API_URL ||
|
|
67
|
-
(nginxEntry ? `${nginxEntry}/api` : `http://127.0.0.1:${apiPort}/api`),
|
|
68
|
-
...(nginxEntry
|
|
69
|
-
? { REACTPRESS_NGINX_ENTRY_URL: nginxEntry, NGINX_ENTRY_URL: nginxEntry }
|
|
70
|
-
: { REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1' }),
|
|
71
|
-
};
|
|
58
|
+
function buildPm2ClientArgs(cmd, cmdArgs) {
|
|
72
59
|
const pm2Mem = getPm2ClientMemoryRestart();
|
|
73
|
-
|
|
60
|
+
return cmd === 'node'
|
|
74
61
|
? [
|
|
75
62
|
'start',
|
|
76
63
|
cmd,
|
|
@@ -79,6 +66,7 @@ function startWithPm2() {
|
|
|
79
66
|
'--update-env',
|
|
80
67
|
'--max-memory-restart',
|
|
81
68
|
pm2Mem,
|
|
69
|
+
'-f',
|
|
82
70
|
'--',
|
|
83
71
|
...cmdArgs,
|
|
84
72
|
]
|
|
@@ -90,16 +78,47 @@ function startWithPm2() {
|
|
|
90
78
|
'--update-env',
|
|
91
79
|
'--max-memory-restart',
|
|
92
80
|
pm2Mem,
|
|
81
|
+
'-f',
|
|
93
82
|
'--',
|
|
94
83
|
...cmdArgs,
|
|
95
84
|
];
|
|
96
|
-
|
|
85
|
+
}
|
|
86
|
+
function startWithPm2() {
|
|
87
|
+
ensureBuilt();
|
|
88
|
+
const [cmd, cmdArgs] = runStartCommand();
|
|
89
|
+
const pm2Env = productionThemeEnv();
|
|
90
|
+
const pm2Bin = resolvePm2Bin(originalCwd) || 'pm2';
|
|
91
|
+
const pm2Args = buildPm2ClientArgs(cmd, cmdArgs);
|
|
92
|
+
const child = spawn(pm2Bin, pm2Args, { stdio: 'inherit', cwd: clientDir, env: pm2Env, shell: cmd !== 'node' });
|
|
97
93
|
child.on('close', (code) => process.exit(code ?? 0));
|
|
98
94
|
child.on('error', (err) => {
|
|
99
|
-
console.error('[ReactPress Client]
|
|
95
|
+
console.error('[ReactPress Client] Failed to start site:', err);
|
|
100
96
|
process.exit(1);
|
|
101
97
|
});
|
|
102
98
|
}
|
|
99
|
+
function startWithPm2Background() {
|
|
100
|
+
ensureBuilt();
|
|
101
|
+
const [cmd, cmdArgs] = runStartCommand();
|
|
102
|
+
const pm2Env = productionThemeEnv();
|
|
103
|
+
const pm2Bin = resolvePm2Bin(originalCwd);
|
|
104
|
+
if (!pm2Bin) {
|
|
105
|
+
startWithNodeBackground();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const result = spawnSync(pm2Bin, buildPm2ClientArgs(cmd, cmdArgs), {
|
|
109
|
+
cwd: clientDir,
|
|
110
|
+
env: pm2Env,
|
|
111
|
+
stdio: 'inherit',
|
|
112
|
+
shell: cmd !== 'node' && process.platform === 'win32',
|
|
113
|
+
});
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
console.warn('[ReactPress Client] Falling back to background node start');
|
|
116
|
+
startWithNodeBackground();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log('[ReactPress Client] Site started in background');
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
103
122
|
function productionThemeEnv() {
|
|
104
123
|
const visitorPort = process.env.CLIENT_PORT || process.env.PORT || '3001';
|
|
105
124
|
const apiPort = process.env.SERVER_PORT || '3002';
|
|
@@ -121,6 +140,49 @@ function productionThemeEnv() {
|
|
|
121
140
|
REACTPRESS_SKIP_DEV_PORT_REDIRECT: '1',
|
|
122
141
|
};
|
|
123
142
|
}
|
|
143
|
+
function resolveBackgroundStartCommand() {
|
|
144
|
+
if (startScript) {
|
|
145
|
+
return { cmd: 'node', args: [startScript], env: {} };
|
|
146
|
+
}
|
|
147
|
+
const env = productionThemeEnv();
|
|
148
|
+
const port = String(env.PORT || '3001');
|
|
149
|
+
const nextBin = path.join(clientDir, 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
150
|
+
if (fs.existsSync(nextBin)) {
|
|
151
|
+
return {
|
|
152
|
+
cmd: process.execPath,
|
|
153
|
+
args: [nextBin, 'start', '-p', port],
|
|
154
|
+
env: { INIT_CWD: clientDir },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { cmd: 'npm', args: ['run', 'start'], env: {}, shell: process.platform === 'win32' };
|
|
158
|
+
}
|
|
159
|
+
function startWithNodeBackground() {
|
|
160
|
+
ensureBuilt();
|
|
161
|
+
const { cmd, args: cmdArgs, env: envExtra = {}, shell = false } = resolveBackgroundStartCommand();
|
|
162
|
+
const logDir = path.join(originalCwd, '.reactpress', 'logs', 'client');
|
|
163
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
164
|
+
const outPath = path.join(logDir, 'stdout.log');
|
|
165
|
+
const errPath = path.join(logDir, 'stderr.log');
|
|
166
|
+
const outFd = fs.openSync(outPath, 'a');
|
|
167
|
+
const errFd = fs.openSync(errPath, 'a');
|
|
168
|
+
const child = spawn(cmd, cmdArgs, {
|
|
169
|
+
detached: true,
|
|
170
|
+
stdio: ['ignore', outFd, errFd],
|
|
171
|
+
cwd: clientDir,
|
|
172
|
+
shell,
|
|
173
|
+
env: {
|
|
174
|
+
...productionThemeEnv(),
|
|
175
|
+
...envExtra,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
child.unref();
|
|
179
|
+
fs.closeSync(outFd);
|
|
180
|
+
fs.closeSync(errFd);
|
|
181
|
+
const { writeClientPid } = require('../lib/process');
|
|
182
|
+
writeClientPid(originalCwd, child.pid);
|
|
183
|
+
console.log(`[ReactPress Client] Started in background (pid ${child.pid})`);
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
124
186
|
function startWithNode() {
|
|
125
187
|
ensureBuilt();
|
|
126
188
|
process.chdir(clientDir);
|
|
@@ -135,6 +197,12 @@ function startWithNode() {
|
|
|
135
197
|
if (usePM2) {
|
|
136
198
|
startWithPm2();
|
|
137
199
|
}
|
|
200
|
+
else if (usePm2Background) {
|
|
201
|
+
startWithPm2Background();
|
|
202
|
+
}
|
|
203
|
+
else if (useBackground) {
|
|
204
|
+
startWithNodeBackground();
|
|
205
|
+
}
|
|
138
206
|
else {
|
|
139
207
|
startWithNode();
|
|
140
208
|
}
|
package/out/bin/reactpress.js
CHANGED
|
@@ -13,8 +13,8 @@ const { brand, divider } = require('../ui/theme');
|
|
|
13
13
|
const { initMonorepoProject, isMonorepoCheckout } = require('../lib/bootstrap');
|
|
14
14
|
const { initProject } = require('../core/services/init');
|
|
15
15
|
const { runLifecycleCommand } = require('../lib/lifecycle');
|
|
16
|
-
const {
|
|
17
|
-
const {
|
|
16
|
+
const { startClientInBackground } = require('../lib/client-lifecycle');
|
|
17
|
+
const { getCliVersion } = require('../lib/paths');
|
|
18
18
|
const { loadServerSiteUrl, loadAdminConsoleUrl, loadClientSiteUrl, getApiPrefix, } = require('../lib/http');
|
|
19
19
|
const { t } = require('../lib/i18n');
|
|
20
20
|
const { describeProject } = require('../lib/project-type');
|
|
@@ -36,6 +36,21 @@ const LEGACY_COMMANDS = new Set([
|
|
|
36
36
|
'desktop',
|
|
37
37
|
'client',
|
|
38
38
|
]);
|
|
39
|
+
function attachLogsOptions(cmd) {
|
|
40
|
+
return cmd
|
|
41
|
+
.option('--tail <n>', t('cli.logs.tailOption'), '50')
|
|
42
|
+
.option('--source <name>', t('cli.logs.sourceOption'), 'error')
|
|
43
|
+
.option('--grep <pattern>', t('cli.logs.grepOption'))
|
|
44
|
+
.option('--list', t('cli.logs.listOption'));
|
|
45
|
+
}
|
|
46
|
+
function createLogsAction() {
|
|
47
|
+
return async (directory, options) => {
|
|
48
|
+
const projectRoot = path.resolve(directory || '.');
|
|
49
|
+
process.env.REACTPRESS_ORIGINAL_CWD = projectRoot;
|
|
50
|
+
const code = await runDoctorLogs(projectRoot, options);
|
|
51
|
+
process.exit(code);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
39
54
|
if (!process.env.REACTPRESS_LOCAL_MODE) {
|
|
40
55
|
process.env.REACTPRESS_LOCAL_MODE = '1';
|
|
41
56
|
}
|
|
@@ -68,26 +83,22 @@ async function startServices(projectRoot) {
|
|
|
68
83
|
process.exit(code);
|
|
69
84
|
const { ensureDefaultTheme } = require('../core/services/theme-bootstrap');
|
|
70
85
|
await ensureDefaultTheme(projectRoot);
|
|
71
|
-
printRunningPanel(projectRoot);
|
|
72
86
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const themeDir = resolveThemeDirectory(projectRoot, activeTheme);
|
|
77
|
-
await runNodeScript(themeBin, [], {
|
|
78
|
-
cwd: projectRoot,
|
|
79
|
-
env: themeDir ? { REACTPRESS_THEME_DIR: themeDir } : undefined,
|
|
80
|
-
});
|
|
87
|
+
const clientCode = await startClientInBackground(projectRoot);
|
|
88
|
+
if (clientCode !== 0)
|
|
89
|
+
process.exit(clientCode);
|
|
81
90
|
}
|
|
82
91
|
catch (err) {
|
|
83
92
|
if (err.code === 'REACTPRESS_THEME_NOT_FOUND' ||
|
|
84
93
|
err.code === 'REACTPRESS_THEME_BIN_NOT_FOUND') {
|
|
94
|
+
printRunningPanel(projectRoot);
|
|
85
95
|
console.log(brand.dim(t('init.apiOnlyHint')));
|
|
86
96
|
return;
|
|
87
97
|
}
|
|
88
98
|
console.error(chalk.red('[reactpress]'), err.message || err);
|
|
89
99
|
process.exit(err.exitCode ?? 1);
|
|
90
100
|
}
|
|
101
|
+
printRunningPanel(projectRoot);
|
|
91
102
|
}
|
|
92
103
|
async function runInit(directory = '.', options = { force: false }) {
|
|
93
104
|
const projectRoot = path.resolve(directory);
|
|
@@ -111,22 +122,22 @@ program
|
|
|
111
122
|
.action(async (directory, options) => {
|
|
112
123
|
await runInit(directory || '.', options);
|
|
113
124
|
});
|
|
125
|
+
attachLogsOptions(program.command('logs [directory]').description(t('cli.logs.description'))).action(createLogsAction());
|
|
126
|
+
program
|
|
127
|
+
.command('stop [directory]')
|
|
128
|
+
.description(t('cli.stop.description'))
|
|
129
|
+
.action(async (directory) => {
|
|
130
|
+
const projectRoot = path.resolve(directory || '.');
|
|
131
|
+
process.env.REACTPRESS_ORIGINAL_CWD = projectRoot;
|
|
132
|
+
await runLifecycleCommand('stop', projectRoot);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
});
|
|
114
135
|
const doctorCommand = program
|
|
115
136
|
.command('doctor')
|
|
116
137
|
.description(t('cli.doctor.description'));
|
|
117
|
-
doctorCommand
|
|
138
|
+
attachLogsOptions(doctorCommand
|
|
118
139
|
.command('logs [directory]')
|
|
119
|
-
.description(t('cli.
|
|
120
|
-
.option('--tail <n>', t('cli.doctor.logs.tailOption'), '50')
|
|
121
|
-
.option('--source <name>', t('cli.doctor.logs.sourceOption'), 'error')
|
|
122
|
-
.option('--grep <pattern>', t('cli.doctor.logs.grepOption'))
|
|
123
|
-
.option('--list', t('cli.doctor.logs.listOption'))
|
|
124
|
-
.action(async (directory, options) => {
|
|
125
|
-
const projectRoot = path.resolve(directory || '.');
|
|
126
|
-
process.env.REACTPRESS_ORIGINAL_CWD = projectRoot;
|
|
127
|
-
const code = await runDoctorLogs(projectRoot, options);
|
|
128
|
-
process.exit(code);
|
|
129
|
-
});
|
|
140
|
+
.description(t('cli.logs.descriptionAlias'))).action(createLogsAction());
|
|
130
141
|
doctorCommand
|
|
131
142
|
.command('check [directory]', { isDefault: true })
|
|
132
143
|
.description(t('cli.doctor.description'))
|
|
@@ -144,7 +155,8 @@ program.on('--help', () => {
|
|
|
144
155
|
for (const line of [
|
|
145
156
|
t('cli.help.init'),
|
|
146
157
|
t('cli.help.doctor'),
|
|
147
|
-
t('cli.help.
|
|
158
|
+
t('cli.help.logs'),
|
|
159
|
+
t('cli.help.stop'),
|
|
148
160
|
t('cli.help.zeroDep'),
|
|
149
161
|
]) {
|
|
150
162
|
console.log(brand.dim(line));
|
|
@@ -123,10 +123,11 @@ async function initLocalProject(projectRoot, options = {}) {
|
|
|
123
123
|
const paths = (0, paths_1.getProjectPaths)(projectRoot);
|
|
124
124
|
const port = options.port ?? 3002;
|
|
125
125
|
if ((await fs_extra_1.default.pathExists(paths.configPath)) && !options.force) {
|
|
126
|
+
const { t } = require('../../lib/i18n');
|
|
126
127
|
return {
|
|
127
|
-
ok:
|
|
128
|
+
ok: true,
|
|
128
129
|
projectRoot,
|
|
129
|
-
message: '
|
|
130
|
+
message: t('init.alreadyInitialized'),
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
133
|
await fs_extra_1.default.ensureDir(projectRoot);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { getThemeBin } = require('./paths');
|
|
6
|
+
const { loadClientSiteUrl } = require('./http');
|
|
7
|
+
const { runNodeScript } = require('./spawn');
|
|
8
|
+
const { readClientPid, isProcessRunning, clearClientPidFile } = require('./process');
|
|
9
|
+
const { PM2_CLIENT_APP, isPm2RuntimeAvailable, isPm2AppOnline, getPm2AppPid, } = require('./pm2-runtime');
|
|
10
|
+
const { t } = require('./i18n');
|
|
11
|
+
function stopClient(projectRoot) {
|
|
12
|
+
const pid = readClientPid(projectRoot);
|
|
13
|
+
if (pid && isProcessRunning(pid)) {
|
|
14
|
+
try {
|
|
15
|
+
process.kill(pid, 'SIGTERM');
|
|
16
|
+
console.log(t('lifecycle.clientStopped', { pid }));
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.warn(t('lifecycle.stopClientPidFailed', { pid }), err.message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
clearClientPidFile(projectRoot);
|
|
23
|
+
}
|
|
24
|
+
async function startClientInBackground(projectRoot) {
|
|
25
|
+
const preferPm2 = isPm2RuntimeAvailable(projectRoot);
|
|
26
|
+
if (preferPm2 && isPm2AppOnline(projectRoot, PM2_CLIENT_APP)) {
|
|
27
|
+
const pid = getPm2AppPid(projectRoot, PM2_CLIENT_APP);
|
|
28
|
+
console.log(t('lifecycle.clientAlreadyRunning', { pid: pid ?? PM2_CLIENT_APP }));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const existing = readClientPid(projectRoot);
|
|
32
|
+
if (!preferPm2 && existing && isProcessRunning(existing)) {
|
|
33
|
+
console.log(t('lifecycle.clientAlreadyRunning', { pid: existing }));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
clearClientPidFile(projectRoot);
|
|
37
|
+
const themeBin = getThemeBin(projectRoot);
|
|
38
|
+
const { resolveThemeDirectory, readActiveThemeManifest } = require('./theme-runtime');
|
|
39
|
+
const { activeTheme } = readActiveThemeManifest(projectRoot);
|
|
40
|
+
const themeDir = resolveThemeDirectory(projectRoot, activeTheme);
|
|
41
|
+
const bgFlag = preferPm2 ? '--pm2-bg' : '--bg';
|
|
42
|
+
await runNodeScript(themeBin, [bgFlag], {
|
|
43
|
+
cwd: projectRoot,
|
|
44
|
+
env: themeDir ? { REACTPRESS_THEME_DIR: themeDir } : undefined,
|
|
45
|
+
});
|
|
46
|
+
const supervised = isPm2AppOnline(projectRoot, PM2_CLIENT_APP);
|
|
47
|
+
const pid = supervised ? getPm2AppPid(projectRoot, PM2_CLIENT_APP) : readClientPid(projectRoot);
|
|
48
|
+
if (!supervised && !pid) {
|
|
49
|
+
console.error(t('lifecycle.clientPidMissing'));
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const clientUrl = loadClientSiteUrl(projectRoot);
|
|
53
|
+
const spinner = ora({
|
|
54
|
+
text: t('lifecycle.waitingClient', { url: clientUrl }),
|
|
55
|
+
color: 'magenta',
|
|
56
|
+
spinner: 'dots',
|
|
57
|
+
}).start();
|
|
58
|
+
const deadline = Date.now() + 180_000;
|
|
59
|
+
let ready = false;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
const alive = supervised
|
|
62
|
+
? isPm2AppOnline(projectRoot, PM2_CLIENT_APP)
|
|
63
|
+
: pid
|
|
64
|
+
? isProcessRunning(pid)
|
|
65
|
+
: false;
|
|
66
|
+
if (!alive) {
|
|
67
|
+
spinner.fail(t('lifecycle.clientExitedEarly', { pid: pid ?? '—' }));
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
if (await require('./http').isHttpResponding(clientUrl)) {
|
|
71
|
+
ready = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
75
|
+
}
|
|
76
|
+
if (!ready) {
|
|
77
|
+
spinner.fail(t('lifecycle.clientTimeout', { url: clientUrl }));
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const readyPid = supervised ? getPm2AppPid(projectRoot, PM2_CLIENT_APP) ?? PM2_CLIENT_APP : pid;
|
|
81
|
+
spinner.succeed(t('lifecycle.clientReady', { url: clientUrl, pid: readyPid }));
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
module.exports = {
|
|
85
|
+
startClientInBackground,
|
|
86
|
+
stopClient,
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=client-lifecycle.js.map
|
package/out/lib/i18n/strings.js
CHANGED
|
@@ -76,6 +76,13 @@ const STRINGS = {
|
|
|
76
76
|
'cli.doctor.logs.sourceOption': 'Log source: error, request, response, or all',
|
|
77
77
|
'cli.doctor.logs.grepOption': 'Filter log lines by regex',
|
|
78
78
|
'cli.doctor.logs.listOption': 'List available log files only',
|
|
79
|
+
'cli.logs.description': 'View recent API logs (error, request, response)',
|
|
80
|
+
'cli.logs.descriptionAlias': 'View API logs (alias of reactpress logs)',
|
|
81
|
+
'cli.logs.tailOption': 'Number of lines to show per log file',
|
|
82
|
+
'cli.logs.sourceOption': 'Log source: error, request, response, or all',
|
|
83
|
+
'cli.logs.grepOption': 'Filter log lines by regex',
|
|
84
|
+
'cli.logs.listOption': 'List available log files only',
|
|
85
|
+
'cli.stop.description': 'Stop API and site services started by init',
|
|
79
86
|
'cli.db.description': 'Database operations',
|
|
80
87
|
'cli.db.backup': 'Backup current project database with mysqldump',
|
|
81
88
|
'cli.db.backup.output': 'Output SQL file path',
|
|
@@ -86,7 +93,9 @@ const STRINGS = {
|
|
|
86
93
|
'cli.help.examples': 'Usage:',
|
|
87
94
|
'cli.help.init': ' reactpress init [dir] Initialize project',
|
|
88
95
|
'cli.help.doctor': ' reactpress doctor Diagnose environment issues',
|
|
89
|
-
'cli.help.
|
|
96
|
+
'cli.help.logs': ' reactpress logs Tail API logs for quick debugging',
|
|
97
|
+
'cli.help.stop': ' reactpress stop Stop the background API',
|
|
98
|
+
'cli.help.doctorLogs': ' reactpress doctor logs Alias of reactpress logs',
|
|
90
99
|
'cli.help.zeroDep': ' Zero deps: Node.js 18+ only — no Docker, nginx, or MySQL',
|
|
91
100
|
'cli.plugin.description': 'Manage ReactPress plugins',
|
|
92
101
|
'cli.plugin.install.description': 'Install a local plugin into .reactpress/plugins',
|
|
@@ -112,10 +121,11 @@ const STRINGS = {
|
|
|
112
121
|
'init.label.site': 'Site',
|
|
113
122
|
'init.label.admin': 'Admin',
|
|
114
123
|
'init.label.api': 'API',
|
|
115
|
-
'init.hint': '
|
|
124
|
+
'init.hint': 'Login: admin / admin · Services run in background · Stop: reactpress stop · Logs: reactpress logs',
|
|
125
|
+
'init.alreadyInitialized': 'Project already initialized — ensuring services are running',
|
|
116
126
|
'init.apiOnlyHint': 'API is running. Install a theme in Admin → Appearance to enable the public site.',
|
|
117
127
|
'init.unknownCommand': 'Unknown command: {cmd}',
|
|
118
|
-
'init.useInitOnly': 'ReactPress 4
|
|
128
|
+
'init.useInitOnly': 'ReactPress 4: reactpress init · doctor · logs · stop',
|
|
119
129
|
'banner.subtitle': '· Publishing platform · v4.0',
|
|
120
130
|
'banner.tagline': 'Zero deps · SQLite · ship in ~60s',
|
|
121
131
|
/** Left label for the decorative pulse bar (not a URL — the repo link
|
|
@@ -415,8 +425,10 @@ const STRINGS = {
|
|
|
415
425
|
'doctor.checking': 'Checking {name}…',
|
|
416
426
|
'doctor.summary': '{passed} passed · {failed} failed · {total} total',
|
|
417
427
|
'doctor.fixesHeader': 'Suggested fixes',
|
|
418
|
-
'doctor.logsHint': 'Tip: run reactpress
|
|
419
|
-
'
|
|
428
|
+
'doctor.logsHint': 'Tip: run reactpress logs --tail 100 to inspect API error logs',
|
|
429
|
+
'logs.title': 'ReactPress Logs',
|
|
430
|
+
'logs.subtitle': 'API log viewer',
|
|
431
|
+
'doctor.logs.title': 'ReactPress Logs',
|
|
420
432
|
'doctor.logs.subtitle': 'API log viewer',
|
|
421
433
|
'doctor.logs.dir': 'Log dir {path}',
|
|
422
434
|
'doctor.logs.empty': 'No API log files found yet.',
|
|
@@ -428,7 +440,7 @@ const STRINGS = {
|
|
|
428
440
|
'doctor.logs.noPid': 'No API pid file at {path}',
|
|
429
441
|
'doctor.logs.pidRunning': 'API pid {pid} (recorded in {path})',
|
|
430
442
|
'doctor.logs.pidStale': 'Stale pid {pid} in {path} — process not running',
|
|
431
|
-
'doctor.logs.moreHint': 'Use --tail, --source, --grep, or --list for more control (see reactpress
|
|
443
|
+
'doctor.logs.moreHint': 'Use --tail, --source, --grep, or --list for more control (see reactpress logs --help)',
|
|
432
444
|
'status.title': 'ReactPress project status',
|
|
433
445
|
'status.dir': 'Project {path}',
|
|
434
446
|
'status.apiSource': 'API source {source}',
|
|
@@ -489,6 +501,14 @@ const STRINGS = {
|
|
|
489
501
|
'lifecycle.apiTimeout120': '[reactpress] API not ready within 120s: {url}',
|
|
490
502
|
'lifecycle.apiExitedEarly': '[reactpress] API process exited early (pid {pid})',
|
|
491
503
|
'lifecycle.apiReady': '[reactpress] API ready: {url}',
|
|
504
|
+
'lifecycle.clientStopped': '[reactpress] Site process stopped (pid {pid})',
|
|
505
|
+
'lifecycle.stopClientPidFailed': '[reactpress] Failed to stop site pid {pid}:',
|
|
506
|
+
'lifecycle.clientAlreadyRunning': '[reactpress] Site already running (pid {pid})',
|
|
507
|
+
'lifecycle.clientPidMissing': '[reactpress] Site failed to start (no pid recorded)',
|
|
508
|
+
'lifecycle.waitingClient': 'Waiting for site at {url}…',
|
|
509
|
+
'lifecycle.clientTimeout': '[reactpress] Site not ready within 180s: {url}',
|
|
510
|
+
'lifecycle.clientExitedEarly': '[reactpress] Site process exited early (pid {pid})',
|
|
511
|
+
'lifecycle.clientReady': '[reactpress] Site ready: {url} (pid {pid})',
|
|
492
512
|
'lifecycle.apiStatusTitle': '[reactpress] API status',
|
|
493
513
|
'lifecycle.source': ' Source: {source}',
|
|
494
514
|
'lifecycle.source.monorepo': 'monorepo server/',
|
|
@@ -500,6 +520,7 @@ const STRINGS = {
|
|
|
500
520
|
'lifecycle.httpReachable': 'reachable',
|
|
501
521
|
'lifecycle.httpUnreachable': 'unreachable',
|
|
502
522
|
'lifecycle.unknownCommand': 'Unknown lifecycle command: {command}',
|
|
523
|
+
'lifecycle.processSupervisorFallback': '[reactpress] Process supervisor unavailable — falling back to background node start',
|
|
503
524
|
'docker.stopping': '[reactpress] Stopping Docker services…',
|
|
504
525
|
'docker.stopped': '[reactpress] Docker services stopped.',
|
|
505
526
|
'docker.stopFailed': '[reactpress] Failed to stop Docker:',
|
|
@@ -730,6 +751,13 @@ const STRINGS = {
|
|
|
730
751
|
'cli.doctor.logs.sourceOption': '日志类型:error、request、response 或 all',
|
|
731
752
|
'cli.doctor.logs.grepOption': '用正则过滤日志行',
|
|
732
753
|
'cli.doctor.logs.listOption': '仅列出可用日志文件',
|
|
754
|
+
'cli.logs.description': '查看 API 日志(error / request / response)',
|
|
755
|
+
'cli.logs.descriptionAlias': '查看 API 日志(同 reactpress logs)',
|
|
756
|
+
'cli.logs.tailOption': '每个日志文件显示的行数',
|
|
757
|
+
'cli.logs.sourceOption': '日志类型:error、request、response 或 all',
|
|
758
|
+
'cli.logs.grepOption': '用正则过滤日志行',
|
|
759
|
+
'cli.logs.listOption': '仅列出可用日志文件',
|
|
760
|
+
'cli.stop.description': '停止 init 启动的 API 与站点服务',
|
|
733
761
|
'cli.db.description': '数据库运维',
|
|
734
762
|
'cli.db.backup': '使用 mysqldump 备份当前项目数据库',
|
|
735
763
|
'cli.db.backup.output': '输出 SQL 文件路径',
|
|
@@ -740,7 +768,9 @@ const STRINGS = {
|
|
|
740
768
|
'cli.help.examples': '用法:',
|
|
741
769
|
'cli.help.init': ' reactpress init [dir] 初始化项目',
|
|
742
770
|
'cli.help.doctor': ' reactpress doctor 诊断环境问题',
|
|
743
|
-
'cli.help.
|
|
771
|
+
'cli.help.logs': ' reactpress logs 查看 API 日志,快速定位问题',
|
|
772
|
+
'cli.help.stop': ' reactpress stop 停止后台 API',
|
|
773
|
+
'cli.help.doctorLogs': ' reactpress doctor logs reactpress logs 的别名',
|
|
744
774
|
'cli.help.zeroDep': ' 零依赖:仅需 Node.js 18+,无需 Docker、nginx 或 MySQL',
|
|
745
775
|
'cli.plugin.description': '管理 ReactPress 插件',
|
|
746
776
|
'cli.plugin.install.description': '安装本地插件到 .reactpress/plugins',
|
|
@@ -766,10 +796,11 @@ const STRINGS = {
|
|
|
766
796
|
'init.label.site': '站点',
|
|
767
797
|
'init.label.admin': '后台',
|
|
768
798
|
'init.label.api': 'API',
|
|
769
|
-
'init.hint': '
|
|
799
|
+
'init.hint': '账号 admin / admin · 服务已在后台运行 · 停止:reactpress stop · 日志:reactpress logs',
|
|
800
|
+
'init.alreadyInitialized': '项目已初始化 — 正在确保服务运行',
|
|
770
801
|
'init.apiOnlyHint': 'API 已运行。请在管理后台「外观 → 主题」安装主题以启用访客站。',
|
|
771
802
|
'init.unknownCommand': '未知命令:{cmd}',
|
|
772
|
-
'init.useInitOnly': 'ReactPress 4 支持:reactpress init ·
|
|
803
|
+
'init.useInitOnly': 'ReactPress 4 支持:reactpress init · doctor · logs · stop',
|
|
773
804
|
'banner.subtitle': '· 发布平台 · v4.0',
|
|
774
805
|
'banner.tagline': '零依赖 · SQLite · 约 60 秒上线',
|
|
775
806
|
/** 左侧装饰性进度条标签(不是网址;仓库地址放在卡片顶部 Title 正下方)。 */
|
|
@@ -1068,8 +1099,10 @@ const STRINGS = {
|
|
|
1068
1099
|
'doctor.checking': '正在检查 {name}…',
|
|
1069
1100
|
'doctor.summary': '通过 {passed} · 失败 {failed} · 共 {total} 项',
|
|
1070
1101
|
'doctor.fixesHeader': '修复建议',
|
|
1071
|
-
'doctor.logsHint': '提示:运行 reactpress
|
|
1072
|
-
'
|
|
1102
|
+
'doctor.logsHint': '提示:运行 reactpress logs --tail 100 查看 API 错误日志',
|
|
1103
|
+
'logs.title': 'ReactPress 日志',
|
|
1104
|
+
'logs.subtitle': 'API 日志查看',
|
|
1105
|
+
'doctor.logs.title': 'ReactPress 日志',
|
|
1073
1106
|
'doctor.logs.subtitle': 'API 日志查看',
|
|
1074
1107
|
'doctor.logs.dir': '日志目录 {path}',
|
|
1075
1108
|
'doctor.logs.empty': '尚未找到 API 日志文件。',
|
|
@@ -1081,7 +1114,7 @@ const STRINGS = {
|
|
|
1081
1114
|
'doctor.logs.noPid': '未找到 API pid 文件:{path}',
|
|
1082
1115
|
'doctor.logs.pidRunning': 'API 进程 pid {pid}(记录在 {path})',
|
|
1083
1116
|
'doctor.logs.pidStale': 'pid 文件 {path} 中的 {pid} 已失效(进程未运行)',
|
|
1084
|
-
'doctor.logs.moreHint': '可用 --tail、--source、--grep、--list 进一步筛选(见 reactpress
|
|
1117
|
+
'doctor.logs.moreHint': '可用 --tail、--source、--grep、--list 进一步筛选(见 reactpress logs --help)',
|
|
1085
1118
|
'status.title': 'ReactPress 项目状态',
|
|
1086
1119
|
'status.dir': '项目目录 {path}',
|
|
1087
1120
|
'status.apiSource': 'API 来源 {source}',
|
|
@@ -1142,6 +1175,14 @@ const STRINGS = {
|
|
|
1142
1175
|
'lifecycle.apiTimeout120': '[reactpress] API 在 120s 内未就绪: {url}',
|
|
1143
1176
|
'lifecycle.apiExitedEarly': '[reactpress] API 进程已提前退出 (pid {pid})',
|
|
1144
1177
|
'lifecycle.apiReady': '[reactpress] API 已就绪: {url}',
|
|
1178
|
+
'lifecycle.clientStopped': '[reactpress] 已停止站点进程 (pid {pid})',
|
|
1179
|
+
'lifecycle.stopClientPidFailed': '[reactpress] 停止站点 pid {pid} 失败:',
|
|
1180
|
+
'lifecycle.clientAlreadyRunning': '[reactpress] 站点已在运行 (pid {pid})',
|
|
1181
|
+
'lifecycle.clientPidMissing': '[reactpress] 站点启动失败(未记录 pid)',
|
|
1182
|
+
'lifecycle.waitingClient': '等待站点就绪 {url}…',
|
|
1183
|
+
'lifecycle.clientTimeout': '[reactpress] 站点在 180s 内未就绪: {url}',
|
|
1184
|
+
'lifecycle.clientExitedEarly': '[reactpress] 站点进程已提前退出 (pid {pid})',
|
|
1185
|
+
'lifecycle.clientReady': '[reactpress] 站点已就绪: {url} (pid {pid})',
|
|
1145
1186
|
'lifecycle.apiStatusTitle': '[reactpress] API 状态',
|
|
1146
1187
|
'lifecycle.source': ' 来源: {source}',
|
|
1147
1188
|
'lifecycle.source.monorepo': 'monorepo server/',
|
|
@@ -1153,6 +1194,7 @@ const STRINGS = {
|
|
|
1153
1194
|
'lifecycle.httpReachable': '可访问',
|
|
1154
1195
|
'lifecycle.httpUnreachable': '不可访问',
|
|
1155
1196
|
'lifecycle.unknownCommand': '未知 lifecycle 命令: {command}',
|
|
1197
|
+
'lifecycle.processSupervisorFallback': '[reactpress] 进程守护不可用 — 已回退到后台 Node 启动',
|
|
1156
1198
|
'docker.stopping': '[reactpress] 正在停止 Docker 服务…',
|
|
1157
1199
|
'docker.stopped': '[reactpress] Docker 服务已停止。',
|
|
1158
1200
|
'docker.stopFailed': '[reactpress] 停止 Docker 失败:',
|