@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/out/lib/lifecycle.js
CHANGED
|
@@ -10,9 +10,12 @@ const { loadServerSiteUrl, waitForHttp } = require('./http');
|
|
|
10
10
|
const { getServerBin, getServerDir, isUsingMonorepoServer, canStartLocalApi, getPidFile, } = require('./paths');
|
|
11
11
|
const net = require('net');
|
|
12
12
|
const { readPid, isProcessRunning, clearPidFile, writePid } = require('./process');
|
|
13
|
+
const { stopClient } = require('./client-lifecycle');
|
|
13
14
|
const { ensureOriginalCwd } = require('./root');
|
|
14
15
|
const { t } = require('./i18n');
|
|
16
|
+
const { getPm2ServerMemoryRestart } = require('./prod-memory');
|
|
15
17
|
const { ensureBundledServerDeps } = require('./server-bundle');
|
|
18
|
+
const { PM2_API_APP, PM2_CLIENT_APP, isPm2RuntimeAvailable, isPm2AppOnline, getPm2AppPid, runPm2, stopPm2Apps, } = require('./pm2-runtime');
|
|
16
19
|
function parseServerPort(projectRoot) {
|
|
17
20
|
try {
|
|
18
21
|
const url = new URL(loadServerSiteUrl(projectRoot));
|
|
@@ -67,51 +70,45 @@ function stopApi(projectRoot) {
|
|
|
67
70
|
}
|
|
68
71
|
clearPidFile(projectRoot);
|
|
69
72
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
clearPidFile(projectRoot);
|
|
80
|
-
if (!canStartLocalApi(projectRoot)) {
|
|
81
|
-
console.error(t('lifecycle.noServerAvailable'));
|
|
82
|
-
return 1;
|
|
83
|
-
}
|
|
84
|
-
if (!isUsingMonorepoServer(projectRoot)) {
|
|
85
|
-
const bundled = await ensureBundledServerDeps(projectRoot);
|
|
86
|
-
if (!bundled.ok) {
|
|
87
|
-
console.error(bundled.message || t('bundle.serverBundle.notBuilt'));
|
|
88
|
-
return 1;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (isUsingMonorepoServer(projectRoot)) {
|
|
92
|
-
console.log(t('lifecycle.startingLocalApi'));
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
console.log(t('lifecycle.startingBundledApi'));
|
|
96
|
-
}
|
|
73
|
+
function stopAll(projectRoot) {
|
|
74
|
+
stopPm2Apps(projectRoot, [PM2_API_APP, PM2_CLIENT_APP]);
|
|
75
|
+
stopClient(projectRoot);
|
|
76
|
+
stopApi(projectRoot);
|
|
77
|
+
}
|
|
78
|
+
async function startApiWithPm2(projectRoot, { wait = true } = {}) {
|
|
79
|
+
const serverBin = getServerBin(projectRoot);
|
|
80
|
+
const serverDir = getServerDir(projectRoot);
|
|
97
81
|
const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server');
|
|
98
82
|
fs.mkdirSync(serverLogDir, { recursive: true });
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
83
|
+
const started = runPm2(projectRoot, [
|
|
84
|
+
'start',
|
|
85
|
+
serverBin,
|
|
86
|
+
'--name',
|
|
87
|
+
PM2_API_APP,
|
|
88
|
+
'--cwd',
|
|
89
|
+
serverDir,
|
|
90
|
+
'--max-memory-restart',
|
|
91
|
+
getPm2ServerMemoryRestart(),
|
|
92
|
+
'--update-env',
|
|
93
|
+
'-f',
|
|
94
|
+
], {
|
|
103
95
|
env: {
|
|
104
|
-
...process.env,
|
|
105
96
|
REACTPRESS_ORIGINAL_CWD: projectRoot,
|
|
106
97
|
REACTPRESS_SERVER_LOG_DIR: serverLogDir,
|
|
107
98
|
},
|
|
99
|
+
stdio: 'ignore',
|
|
108
100
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!wait) {
|
|
113
|
-
return 0;
|
|
101
|
+
if (!started.ok) {
|
|
102
|
+
console.warn(t('lifecycle.processSupervisorFallback'));
|
|
103
|
+
return startApiDetached(projectRoot, { wait });
|
|
114
104
|
}
|
|
105
|
+
const pid = getPm2AppPid(projectRoot, PM2_API_APP);
|
|
106
|
+
console.log(t('lifecycle.apiStartedBg', { pid: pid ?? PM2_API_APP }));
|
|
107
|
+
if (!wait)
|
|
108
|
+
return 0;
|
|
109
|
+
return waitForApiReady(projectRoot, pid);
|
|
110
|
+
}
|
|
111
|
+
async function waitForApiReady(projectRoot, pid) {
|
|
115
112
|
const serverUrl = loadServerSiteUrl(projectRoot);
|
|
116
113
|
const spinner = ora({
|
|
117
114
|
text: t('dev.waitingApi', { url: serverUrl }),
|
|
@@ -121,8 +118,8 @@ async function startApi(projectRoot, { wait = true } = {}) {
|
|
|
121
118
|
const deadline = Date.now() + 120_000;
|
|
122
119
|
let ready = false;
|
|
123
120
|
while (Date.now() < deadline) {
|
|
124
|
-
if (!isProcessRunning(
|
|
125
|
-
spinner.fail(t('lifecycle.apiExitedEarly', { pid
|
|
121
|
+
if (pid && !isProcessRunning(pid) && !isPm2AppOnline(projectRoot, PM2_API_APP)) {
|
|
122
|
+
spinner.fail(t('lifecycle.apiExitedEarly', { pid }));
|
|
126
123
|
return 1;
|
|
127
124
|
}
|
|
128
125
|
if (await require('./http').isHttpResponding(serverUrl)) {
|
|
@@ -138,6 +135,63 @@ async function startApi(projectRoot, { wait = true } = {}) {
|
|
|
138
135
|
spinner.succeed(t('lifecycle.apiReady', { url: serverUrl }));
|
|
139
136
|
return 0;
|
|
140
137
|
}
|
|
138
|
+
async function startApiDetached(projectRoot, { wait = true } = {}) {
|
|
139
|
+
const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server');
|
|
140
|
+
fs.mkdirSync(serverLogDir, { recursive: true });
|
|
141
|
+
const child = spawn(process.execPath, [getServerBin(projectRoot)], {
|
|
142
|
+
cwd: getServerDir(projectRoot),
|
|
143
|
+
detached: true,
|
|
144
|
+
stdio: 'ignore',
|
|
145
|
+
env: {
|
|
146
|
+
...process.env,
|
|
147
|
+
REACTPRESS_ORIGINAL_CWD: projectRoot,
|
|
148
|
+
REACTPRESS_SERVER_LOG_DIR: serverLogDir,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
child.unref();
|
|
152
|
+
writePid(projectRoot, child.pid);
|
|
153
|
+
console.log(t('lifecycle.apiStartedBg', { pid: child.pid }));
|
|
154
|
+
if (!wait)
|
|
155
|
+
return 0;
|
|
156
|
+
return waitForApiReady(projectRoot, child.pid);
|
|
157
|
+
}
|
|
158
|
+
async function startApi(projectRoot, { wait = true } = {}) {
|
|
159
|
+
if (!(await ensureConfig(projectRoot))) {
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
if (isPm2AppOnline(projectRoot, PM2_API_APP)) {
|
|
163
|
+
const pid = getPm2AppPid(projectRoot, PM2_API_APP);
|
|
164
|
+
console.log(t('lifecycle.apiAlreadyRunning', { pid: pid ?? PM2_API_APP }));
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
const existing = readPid(projectRoot);
|
|
168
|
+
if (existing && isProcessRunning(existing)) {
|
|
169
|
+
console.log(t('lifecycle.apiAlreadyRunning', { pid: existing }));
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
clearPidFile(projectRoot);
|
|
173
|
+
if (!canStartLocalApi(projectRoot)) {
|
|
174
|
+
console.error(t('lifecycle.noServerAvailable'));
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
if (!isUsingMonorepoServer(projectRoot)) {
|
|
178
|
+
const bundled = await ensureBundledServerDeps(projectRoot);
|
|
179
|
+
if (!bundled.ok) {
|
|
180
|
+
console.error(bundled.message || t('bundle.serverBundle.notBuilt'));
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (isUsingMonorepoServer(projectRoot)) {
|
|
185
|
+
console.log(t('lifecycle.startingLocalApi'));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log(t('lifecycle.startingBundledApi'));
|
|
189
|
+
}
|
|
190
|
+
if (isPm2RuntimeAvailable(projectRoot)) {
|
|
191
|
+
return startApiWithPm2(projectRoot, { wait });
|
|
192
|
+
}
|
|
193
|
+
return startApiDetached(projectRoot, { wait });
|
|
194
|
+
}
|
|
141
195
|
async function statusApi(projectRoot) {
|
|
142
196
|
const pid = readPid(projectRoot);
|
|
143
197
|
const serverUrl = loadServerSiteUrl(projectRoot);
|
|
@@ -171,10 +225,10 @@ async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) {
|
|
|
171
225
|
case 'start:bg':
|
|
172
226
|
return startApi(projectRoot, { wait: false });
|
|
173
227
|
case 'stop':
|
|
174
|
-
|
|
228
|
+
stopAll(projectRoot);
|
|
175
229
|
return 0;
|
|
176
230
|
case 'restart':
|
|
177
|
-
|
|
231
|
+
stopAll(projectRoot);
|
|
178
232
|
await waitForPortFree(parseServerPort(projectRoot));
|
|
179
233
|
await new Promise((r) => setTimeout(r, 400));
|
|
180
234
|
return startApi(projectRoot, { wait: true });
|
|
@@ -188,6 +242,7 @@ async function runLifecycleCommand(command, projectRoot = ensureOriginalCwd()) {
|
|
|
188
242
|
module.exports = {
|
|
189
243
|
startApi,
|
|
190
244
|
stopApi,
|
|
245
|
+
stopAll,
|
|
191
246
|
statusApi,
|
|
192
247
|
runLifecycleCommand,
|
|
193
248
|
};
|
package/out/lib/paths.js
CHANGED
|
@@ -95,6 +95,9 @@ function getClientBin(projectRoot) {
|
|
|
95
95
|
function getPidFile(projectRoot) {
|
|
96
96
|
return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'server.pid');
|
|
97
97
|
}
|
|
98
|
+
function getClientPidFile(projectRoot) {
|
|
99
|
+
return path.join(resolveProjectRoot(projectRoot), '.reactpress', 'client.pid');
|
|
100
|
+
}
|
|
98
101
|
module.exports = {
|
|
99
102
|
getMonorepoRoot,
|
|
100
103
|
resolveProjectRoot,
|
|
@@ -113,5 +116,6 @@ module.exports = {
|
|
|
113
116
|
getThemeBin,
|
|
114
117
|
getClientBin,
|
|
115
118
|
getPidFile,
|
|
119
|
+
getClientPidFile,
|
|
116
120
|
};
|
|
117
121
|
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { getBundledServerDir, getServerDir } = require('./paths');
|
|
8
|
+
/** Internal PM2 app names — not exposed in user-facing CLI copy. */
|
|
9
|
+
const PM2_API_APP = 'reactpress-api';
|
|
10
|
+
const PM2_CLIENT_APP = 'reactpress-client';
|
|
11
|
+
function isPm2Disabled() {
|
|
12
|
+
return process.env.REACTPRESS_DISABLE_PM2 === '1';
|
|
13
|
+
}
|
|
14
|
+
function resolvePm2Bin(projectRoot) {
|
|
15
|
+
if (isPm2Disabled())
|
|
16
|
+
return null;
|
|
17
|
+
const candidates = [
|
|
18
|
+
path.join(getServerDir(projectRoot), 'node_modules', '.bin', 'pm2'),
|
|
19
|
+
path.join(getBundledServerDir(), 'node_modules', '.bin', 'pm2'),
|
|
20
|
+
];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (fs.existsSync(candidate))
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
const probe = spawnSync('pm2', ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
|
|
26
|
+
return probe.status === 0 ? 'pm2' : null;
|
|
27
|
+
}
|
|
28
|
+
function isPm2RuntimeAvailable(projectRoot) {
|
|
29
|
+
return Boolean(resolvePm2Bin(projectRoot));
|
|
30
|
+
}
|
|
31
|
+
function runPm2(projectRoot, args, options = {}) {
|
|
32
|
+
const bin = resolvePm2Bin(projectRoot);
|
|
33
|
+
if (!bin) {
|
|
34
|
+
return { ok: false, error: 'PM2_UNAVAILABLE' };
|
|
35
|
+
}
|
|
36
|
+
const result = spawnSync(bin, args, {
|
|
37
|
+
cwd: options.cwd,
|
|
38
|
+
env: { ...process.env, ...options.env },
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: options.stdio ?? 'pipe',
|
|
41
|
+
shell: options.shell ?? false,
|
|
42
|
+
});
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
const message = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
45
|
+
return { ok: false, error: message || `pm2 exited ${result.status}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, stdout: result.stdout ?? '' };
|
|
48
|
+
}
|
|
49
|
+
function readPm2Apps(projectRoot) {
|
|
50
|
+
const result = runPm2(projectRoot, ['jlist'], { stdio: 'pipe' });
|
|
51
|
+
if (!result.ok)
|
|
52
|
+
return [];
|
|
53
|
+
try {
|
|
54
|
+
const apps = JSON.parse(result.stdout);
|
|
55
|
+
return Array.isArray(apps) ? apps : [];
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getPm2App(projectRoot, name) {
|
|
62
|
+
return readPm2Apps(projectRoot).find((app) => app?.name === name) ?? null;
|
|
63
|
+
}
|
|
64
|
+
function isPm2AppOnline(projectRoot, name) {
|
|
65
|
+
const app = getPm2App(projectRoot, name);
|
|
66
|
+
return app?.pm2_env?.status === 'online';
|
|
67
|
+
}
|
|
68
|
+
function getPm2AppPid(projectRoot, name) {
|
|
69
|
+
const app = getPm2App(projectRoot, name);
|
|
70
|
+
return typeof app?.pid === 'number' ? app.pid : null;
|
|
71
|
+
}
|
|
72
|
+
function stopPm2Apps(projectRoot, names) {
|
|
73
|
+
const bin = resolvePm2Bin(projectRoot);
|
|
74
|
+
if (!bin)
|
|
75
|
+
return false;
|
|
76
|
+
const targets = names.filter((name) => getPm2App(projectRoot, name));
|
|
77
|
+
if (!targets.length)
|
|
78
|
+
return false;
|
|
79
|
+
runPm2(projectRoot, ['delete', ...targets], { stdio: 'ignore' });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
module.exports = {
|
|
83
|
+
PM2_API_APP,
|
|
84
|
+
PM2_CLIENT_APP,
|
|
85
|
+
isPm2Disabled,
|
|
86
|
+
resolvePm2Bin,
|
|
87
|
+
isPm2RuntimeAvailable,
|
|
88
|
+
runPm2,
|
|
89
|
+
readPm2Apps,
|
|
90
|
+
getPm2App,
|
|
91
|
+
isPm2AppOnline,
|
|
92
|
+
getPm2AppPid,
|
|
93
|
+
stopPm2Apps,
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=pm2-runtime.js.map
|
package/out/lib/process.js
CHANGED
|
@@ -3,9 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
// @ts-nocheck
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { getPidFile } = require('./paths');
|
|
7
|
-
function
|
|
8
|
-
const pidFile = getPidFile(projectRoot);
|
|
6
|
+
const { getPidFile, getClientPidFile } = require('./paths');
|
|
7
|
+
function readPidFromFile(pidFile) {
|
|
9
8
|
try {
|
|
10
9
|
const raw = fs.readFileSync(pidFile, 'utf8').trim();
|
|
11
10
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -15,6 +14,12 @@ function readPid(projectRoot) {
|
|
|
15
14
|
return null;
|
|
16
15
|
}
|
|
17
16
|
}
|
|
17
|
+
function readPid(projectRoot) {
|
|
18
|
+
return readPidFromFile(getPidFile(projectRoot));
|
|
19
|
+
}
|
|
20
|
+
function readClientPid(projectRoot) {
|
|
21
|
+
return readPidFromFile(getClientPidFile(projectRoot));
|
|
22
|
+
}
|
|
18
23
|
function isProcessRunning(pid) {
|
|
19
24
|
if (!pid)
|
|
20
25
|
return false;
|
|
@@ -32,16 +37,31 @@ function clearPidFile(projectRoot) {
|
|
|
32
37
|
fs.unlinkSync(pidFile);
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
|
-
function
|
|
36
|
-
const pidFile =
|
|
40
|
+
function clearClientPidFile(projectRoot) {
|
|
41
|
+
const pidFile = getClientPidFile(projectRoot);
|
|
42
|
+
if (fs.existsSync(pidFile)) {
|
|
43
|
+
fs.unlinkSync(pidFile);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writePidToFile(pidFile, pid) {
|
|
37
47
|
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
38
48
|
fs.writeFileSync(pidFile, String(pid));
|
|
39
49
|
}
|
|
50
|
+
function writePid(projectRoot, pid) {
|
|
51
|
+
writePidToFile(getPidFile(projectRoot), pid);
|
|
52
|
+
}
|
|
53
|
+
function writeClientPid(projectRoot, pid) {
|
|
54
|
+
writePidToFile(getClientPidFile(projectRoot), pid);
|
|
55
|
+
}
|
|
40
56
|
module.exports = {
|
|
41
57
|
readPid,
|
|
58
|
+
readClientPid,
|
|
42
59
|
isProcessRunning,
|
|
43
60
|
clearPidFile,
|
|
61
|
+
clearClientPidFile,
|
|
44
62
|
writePid,
|
|
63
|
+
writeClientPid,
|
|
45
64
|
getPidFile,
|
|
65
|
+
getClientPidFile,
|
|
46
66
|
};
|
|
47
67
|
//# sourceMappingURL=process.js.map
|
package/out/lib/project-logs.js
CHANGED
|
@@ -122,7 +122,7 @@ async function runDoctorLogs(projectRoot, options = {}) {
|
|
|
122
122
|
const files = listLogFiles(logDir, source);
|
|
123
123
|
const w = 56;
|
|
124
124
|
console.log('');
|
|
125
|
-
console.log(` ${gradientText(t('
|
|
125
|
+
console.log(` ${gradientText(t('logs.title'), [palette.primary, palette.accent], { bold: true })} ${brand.dim(t('logs.subtitle'))}`);
|
|
126
126
|
console.log(` ${brand.dim(t('doctor.project', { path: projectRoot }))}`);
|
|
127
127
|
console.log(` ${brand.dim(t('doctor.logs.dir', { path: logDir }))}`);
|
|
128
128
|
printProcessHint(projectRoot);
|
package/out/lib/publish.js
CHANGED
|
@@ -16,12 +16,12 @@ const CORE_PUBLISH_PACKAGES = [
|
|
|
16
16
|
{
|
|
17
17
|
name: '@fecommunity/reactpress-toolkit',
|
|
18
18
|
path: 'toolkit',
|
|
19
|
-
description: 'API
|
|
19
|
+
description: 'Official TypeScript SDK — API clients, theme SSR, plugin hooks',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
name: '@fecommunity/reactpress-web',
|
|
23
23
|
path: 'web',
|
|
24
|
-
description: 'Admin SPA
|
|
24
|
+
description: 'Official Admin SPA — WordPress-style writing UI',
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: '@fecommunity/reactpress-server',
|
|
@@ -32,7 +32,7 @@ const CORE_PUBLISH_PACKAGES = [
|
|
|
32
32
|
{
|
|
33
33
|
name: '@fecommunity/reactpress',
|
|
34
34
|
path: 'cli',
|
|
35
|
-
description:
|
|
35
|
+
description: 'Publishing platform CLI — CMS, Admin, API, themes in one command',
|
|
36
36
|
},
|
|
37
37
|
];
|
|
38
38
|
function getWorkspaceRoot() {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const PATCH_MARKER = '.reactpress-api-proxy-patched';
|
|
7
|
+
const ENCODING_PATCH_MARKER = 'reactpress-api-proxy-encoding';
|
|
8
|
+
const ROUTE_REL = path.join('app', 'api', '[[...path]]', 'route.ts');
|
|
9
|
+
const PROXY_REL = path.join('lib', 'mock-api', 'proxy.ts');
|
|
10
|
+
/** Buggy pattern: body read before mock/proxy branch (breaks upstream proxy). */
|
|
11
|
+
const BUGGY_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
|
|
12
|
+
const body = await readJsonBody(request)
|
|
13
|
+
|
|
14
|
+
let response: Response
|
|
15
|
+
if (isMockApiEnabled()) {
|
|
16
|
+
const mockResponse = matchMockRoute(request.method, path, { keyword, body })`;
|
|
17
|
+
const FIXED_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
|
|
18
|
+
|
|
19
|
+
let response: Response
|
|
20
|
+
if (isMockApiEnabled()) {
|
|
21
|
+
const body = await readJsonBody(request)
|
|
22
|
+
const mockResponse = matchMockRoute(request.method, path, { keyword, body })`;
|
|
23
|
+
const BUGGY_PROXY_BLOCK = ` return new Response(upstream.body, {
|
|
24
|
+
status: upstream.status,
|
|
25
|
+
headers: upstream.headers,
|
|
26
|
+
})`;
|
|
27
|
+
const FIXED_PROXY_BLOCK = ` const responseHeaders = new Headers(upstream.headers)
|
|
28
|
+
// ${ENCODING_PATCH_MARKER}: Node fetch decompresses bodies; strip stale encoding headers.
|
|
29
|
+
responseHeaders.delete('content-encoding')
|
|
30
|
+
responseHeaders.delete('content-length')
|
|
31
|
+
responseHeaders.delete('transfer-encoding')
|
|
32
|
+
|
|
33
|
+
return new Response(upstream.body, {
|
|
34
|
+
status: upstream.status,
|
|
35
|
+
headers: responseHeaders,
|
|
36
|
+
})`;
|
|
37
|
+
function resolveApiRoutePath(themeDir) {
|
|
38
|
+
if (!themeDir)
|
|
39
|
+
return null;
|
|
40
|
+
const routePath = path.join(themeDir, ROUTE_REL);
|
|
41
|
+
return fs.existsSync(routePath) ? routePath : null;
|
|
42
|
+
}
|
|
43
|
+
function resolveApiProxyPath(themeDir) {
|
|
44
|
+
if (!themeDir)
|
|
45
|
+
return null;
|
|
46
|
+
const proxyPath = path.join(themeDir, PROXY_REL);
|
|
47
|
+
return fs.existsSync(proxyPath) ? proxyPath : null;
|
|
48
|
+
}
|
|
49
|
+
function touchPatchMarker(themeDir) {
|
|
50
|
+
fs.writeFileSync(path.join(themeDir, PATCH_MARKER), `${new Date().toISOString()}\n`, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
function patchThemeApiRouteBodyRead(themeDir) {
|
|
53
|
+
const routePath = resolveApiRoutePath(themeDir);
|
|
54
|
+
if (!routePath)
|
|
55
|
+
return false;
|
|
56
|
+
let src = fs.readFileSync(routePath, 'utf8');
|
|
57
|
+
if (src.includes(FIXED_ROUTE_BLOCK) || !src.includes(BUGGY_ROUTE_BLOCK)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
src = src.replace(BUGGY_ROUTE_BLOCK, FIXED_ROUTE_BLOCK);
|
|
61
|
+
fs.writeFileSync(routePath, src, 'utf8');
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Node fetch() auto-decompresses upstream bodies but leaves Content-Encoding on the Response.
|
|
66
|
+
* Browsers then fail with ERR_CONTENT_DECODING_FAILED (HTTP 200).
|
|
67
|
+
*/
|
|
68
|
+
function patchThemeApiProxyEncodingHeaders(themeDir) {
|
|
69
|
+
const proxyPath = resolveApiProxyPath(themeDir);
|
|
70
|
+
if (!proxyPath)
|
|
71
|
+
return false;
|
|
72
|
+
let src = fs.readFileSync(proxyPath, 'utf8');
|
|
73
|
+
if (src.includes(ENCODING_PATCH_MARKER) || !src.includes(BUGGY_PROXY_BLOCK)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
src = src.replace(BUGGY_PROXY_BLOCK, FIXED_PROXY_BLOCK);
|
|
77
|
+
fs.writeFileSync(proxyPath, src, 'utf8');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* App Router themes proxy /api via app/api/[[...path]]/route.ts.
|
|
82
|
+
* Patches known theme-starter issues before production build.
|
|
83
|
+
*/
|
|
84
|
+
function patchThemeApiProxyRoute(themeDir) {
|
|
85
|
+
const routePatched = patchThemeApiRouteBodyRead(themeDir);
|
|
86
|
+
const proxyPatched = patchThemeApiProxyEncodingHeaders(themeDir);
|
|
87
|
+
if (routePatched || proxyPatched) {
|
|
88
|
+
touchPatchMarker(themeDir);
|
|
89
|
+
}
|
|
90
|
+
return routePatched || proxyPatched;
|
|
91
|
+
}
|
|
92
|
+
module.exports = {
|
|
93
|
+
PATCH_MARKER,
|
|
94
|
+
ENCODING_PATCH_MARKER,
|
|
95
|
+
ROUTE_REL,
|
|
96
|
+
PROXY_REL,
|
|
97
|
+
patchThemeApiProxyRoute,
|
|
98
|
+
patchThemeApiRouteBodyRead,
|
|
99
|
+
patchThemeApiProxyEncodingHeaders,
|
|
100
|
+
resolveApiRoutePath,
|
|
101
|
+
resolveApiProxyPath,
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=theme-api-proxy-patch.js.map
|
package/out/lib/theme-install.js
CHANGED
|
@@ -9,6 +9,7 @@ const { spawnSync } = require('child_process');
|
|
|
9
9
|
const { upsertNpmThemeLock } = require('./theme-lock');
|
|
10
10
|
const { buildThemeEnvOverrides, syncThemeEnvFromProject } = require('./theme-env');
|
|
11
11
|
const { ensurePreviewFrameAllowed } = require('./theme-preview-frame');
|
|
12
|
+
const { patchThemeApiProxyRoute } = require('./theme-api-proxy-patch');
|
|
12
13
|
const { resolveCatalogInstallSpec } = require('./theme-registry');
|
|
13
14
|
const THEME_ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
14
15
|
const THEME_RUNTIME_REL = path.join('.reactpress', 'runtime');
|
|
@@ -332,6 +333,7 @@ async function installThemeFromNpm(projectRoot, spec, options = {}) {
|
|
|
332
333
|
syncThemeEnvFromProject(root, targetDir);
|
|
333
334
|
}
|
|
334
335
|
ensurePreviewFrameAllowed(targetDir);
|
|
336
|
+
patchThemeApiProxyRoute(targetDir);
|
|
335
337
|
const npmSpec = parsed.kind === 'npm' ? parsed.spec : resolvedSpec;
|
|
336
338
|
upsertNpmThemeLock(root, themeId, {
|
|
337
339
|
spec: npmSpec,
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fecommunity/reactpress",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
4
|
-
"description": "ReactPress
|
|
3
|
+
"version": "4.0.0-beta.15",
|
|
4
|
+
"description": "ReactPress CLI — publishing platform for modern developers. CMS, Admin, API, and Next.js themes in one command. Self-hosted, MIT. `npx @fecommunity/reactpress init`",
|
|
5
5
|
"author": "fecommunity",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://reactpress-docs.vercel.app/",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/fecommunity/reactpress/issues"
|
|
10
|
+
},
|
|
7
11
|
"repository": {
|
|
8
12
|
"type": "git",
|
|
9
13
|
"url": "git+https://github.com/fecommunity/reactpress.git",
|
|
@@ -13,7 +17,18 @@
|
|
|
13
17
|
"reactpress",
|
|
14
18
|
"cms",
|
|
15
19
|
"blog",
|
|
16
|
-
"
|
|
20
|
+
"blog-platform",
|
|
21
|
+
"cli",
|
|
22
|
+
"headless-cms",
|
|
23
|
+
"publishing-platform",
|
|
24
|
+
"wordpress-alternative",
|
|
25
|
+
"self-hosted",
|
|
26
|
+
"sqlite",
|
|
27
|
+
"nestjs",
|
|
28
|
+
"nextjs",
|
|
29
|
+
"content-management",
|
|
30
|
+
"typescript",
|
|
31
|
+
"open-source"
|
|
17
32
|
],
|
|
18
33
|
"engines": {
|
|
19
34
|
"node": ">=18"
|
package/server/package-lock.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fecommunity/reactpress-server",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.15",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@fecommunity/reactpress-server",
|
|
9
|
-
"version": "4.0.0-beta.
|
|
10
|
-
"deprecated": "
|
|
9
|
+
"version": "4.0.0-beta.15",
|
|
10
|
+
"deprecated": "Bundled in @fecommunity/reactpress CLI from v3.1. Run `npx @fecommunity/reactpress init` for new projects.",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@fecommunity/reactpress-toolkit": "file:../toolkit",
|
|
@@ -86,12 +86,12 @@
|
|
|
86
86
|
"typescript": "~4.9.5"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
|
-
"node": ">=
|
|
89
|
+
"node": ">=18"
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
"../toolkit": {
|
|
93
93
|
"name": "@fecommunity/reactpress-toolkit",
|
|
94
|
-
"version": "4.0.0-beta.
|
|
94
|
+
"version": "4.0.0-beta.15",
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"ajv": "^8.17.1",
|
|
97
97
|
"axios": "^1.12.2",
|
package/server/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fecommunity/reactpress-server",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
4
|
-
"deprecated": "
|
|
5
|
-
"description": "ReactPress NestJS
|
|
3
|
+
"version": "4.0.0-beta.15",
|
|
4
|
+
"deprecated": "Bundled in @fecommunity/reactpress CLI from v3.1. Run `npx @fecommunity/reactpress init` for new projects.",
|
|
5
|
+
"description": "ReactPress NestJS REST API (deprecated). Bundled in @fecommunity/reactpress CLI from v3.1. Headless CMS for articles, media, plugins, and themes",
|
|
6
6
|
"author": "fecommunity",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"homepage": "https://reactpress-docs.vercel.app/",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/fecommunity/reactpress/issues"
|
|
11
|
+
},
|
|
8
12
|
"main": "dist/main.js",
|
|
9
|
-
"types": "dist/index.d.ts",
|
|
10
13
|
"bin": {
|
|
11
14
|
"reactpress-server": "./bin/reactpress-server.js"
|
|
12
15
|
},
|
|
@@ -19,20 +22,28 @@
|
|
|
19
22
|
],
|
|
20
23
|
"keywords": [
|
|
21
24
|
"reactpress",
|
|
22
|
-
"server",
|
|
23
25
|
"nestjs",
|
|
26
|
+
"headless-cms",
|
|
24
27
|
"cms",
|
|
25
28
|
"blog",
|
|
29
|
+
"rest-api",
|
|
26
30
|
"api",
|
|
27
|
-
"
|
|
31
|
+
"openapi",
|
|
32
|
+
"swagger",
|
|
33
|
+
"typescript",
|
|
34
|
+
"self-hosted"
|
|
28
35
|
],
|
|
29
36
|
"repository": {
|
|
30
37
|
"type": "git",
|
|
31
38
|
"url": "git+https://github.com/fecommunity/reactpress.git",
|
|
32
39
|
"directory": "server"
|
|
33
40
|
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
34
45
|
"engines": {
|
|
35
|
-
"node": ">=
|
|
46
|
+
"node": ">=18"
|
|
36
47
|
},
|
|
37
48
|
"scripts": {
|
|
38
49
|
"clean": "rimraf dist",
|
|
@@ -41,9 +41,12 @@ function applyThemeWebpackResolve(config, appDir) {
|
|
|
41
41
|
* Pass overrides to extend (your `webpack` runs after the supports-color alias).
|
|
42
42
|
*/
|
|
43
43
|
function createReactPressNextConfig(overrides = {}) {
|
|
44
|
-
const { webpack: userWebpack, eslint, transpilePackages, ...rest } = overrides;
|
|
44
|
+
const { webpack: userWebpack, eslint, transpilePackages, compress, ...rest } = overrides;
|
|
45
45
|
return {
|
|
46
46
|
reactStrictMode: true,
|
|
47
|
+
// Themes rewrite `/api` to Nest. Next's compress middleware decompresses upstream
|
|
48
|
+
// bodies but can still emit `Content-Encoding: br`, breaking admin fetch (ERR_CONTENT_DECODING_FAILED).
|
|
49
|
+
compress: compress ?? false,
|
|
47
50
|
transpilePackages: ['@fecommunity/reactpress-toolkit', ...(transpilePackages ?? [])],
|
|
48
51
|
eslint: {
|
|
49
52
|
ignoreDuringBuilds: true,
|