react-on-rails-pro-node-renderer 17.0.0-rc.5 → 17.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/integrations/opentelemetry.js +4 -4
- package/lib/master/restartWorkers.js +82 -28
- package/lib/master.js +20 -3
- package/lib/shared/checkRscPeerCompatibility.d.ts +1 -2
- package/lib/shared/checkRscPeerCompatibility.js +77 -22
- package/lib/shared/configBuilder.js +25 -17
- package/lib/shared/licenseValidator.js +1 -0
- package/lib/shared/rscPeerSupport.d.ts +2 -5
- package/lib/shared/rscPeerSupport.js +10 -10
- package/lib/shared/runRscPeerCompatibilityCheck.js +0 -5
- package/lib/shared/sharedConsoleHistory.js +3 -0
- package/lib/shared/utils.d.ts +2 -0
- package/lib/shared/utils.js +44 -4
- package/lib/worker/handleGracefulShutdown.js +33 -13
- package/lib/worker/handleIncrementalRenderRequest.d.ts +10 -0
- package/lib/worker/handleIncrementalRenderRequest.js +196 -36
- package/lib/worker/handleIncrementalRenderStream.d.ts +3 -0
- package/lib/worker/handleIncrementalRenderStream.js +79 -5
- package/lib/worker/handleRenderRequest.d.ts +5 -1
- package/lib/worker/handleRenderRequest.js +27 -9
- package/lib/worker/shutdownHooks.js +1 -1
- package/lib/worker/streamingUtils.d.ts +3 -0
- package/lib/worker/streamingUtils.js +38 -0
- package/lib/worker/vm.d.ts +1 -0
- package/lib/worker/vm.js +1 -0
- package/lib/worker.d.ts +1 -0
- package/lib/worker.js +225 -13
- package/package.json +7 -9
|
@@ -17,10 +17,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.init = init;
|
|
18
18
|
const api_js_1 = require("./api.js");
|
|
19
19
|
const DEFAULT_SERVICE_NAME = 'react-on-rails-pro-node-renderer';
|
|
20
|
-
const DEFAULT_SHUTDOWN_TIMEOUT_MS =
|
|
20
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
21
21
|
// Leave 1s of headroom under the worker's hard cap so the shutdown hook can
|
|
22
22
|
// resolve cleanly even when provider.shutdown() runs right at its limit.
|
|
23
|
-
const MAX_SHUTDOWN_TIMEOUT_MS = api_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS -
|
|
23
|
+
const MAX_SHUTDOWN_TIMEOUT_MS = api_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS - 1_000;
|
|
24
24
|
function isProduction() {
|
|
25
25
|
return process.env.NODE_ENV === 'production' || process.env.RAILS_ENV === 'production';
|
|
26
26
|
}
|
|
@@ -265,7 +265,7 @@ function init(opts = {}) {
|
|
|
265
265
|
}
|
|
266
266
|
let shutdownOpenTelemetryPromise;
|
|
267
267
|
const shutdownOpenTelemetry = () => {
|
|
268
|
-
shutdownOpenTelemetryPromise
|
|
268
|
+
shutdownOpenTelemetryPromise ??= (async () => {
|
|
269
269
|
try {
|
|
270
270
|
await shutdownProviderWithTimeout(provider, shutdownTimeoutMs);
|
|
271
271
|
if ((0, api_js_1.getOpenTelemetryTracerProvider)() === provider) {
|
|
@@ -279,7 +279,7 @@ function init(opts = {}) {
|
|
|
279
279
|
unregisterFastifyConfig?.();
|
|
280
280
|
unregisterWorkerShutdownHook?.();
|
|
281
281
|
}
|
|
282
|
-
})()
|
|
282
|
+
})();
|
|
283
283
|
return shutdownOpenTelemetryPromise;
|
|
284
284
|
};
|
|
285
285
|
// Register these last so failed init paths do not leave partial shutdown hooks
|
|
@@ -25,39 +25,93 @@ exports.default = restartWorkers;
|
|
|
25
25
|
const cluster_1 = __importDefault(require("cluster"));
|
|
26
26
|
const log_js_1 = __importDefault(require("../shared/log.js"));
|
|
27
27
|
const utils_js_1 = require("../shared/utils.js");
|
|
28
|
+
const MILLISECONDS_IN_SECOND = 1000;
|
|
28
29
|
const MILLISECONDS_IN_MINUTE = 60000;
|
|
30
|
+
function currentWorker(workerId) {
|
|
31
|
+
const worker = cluster_1.default.workers?.[workerId];
|
|
32
|
+
if (!worker) {
|
|
33
|
+
log_js_1.default.debug('Worker #%s is no longer available for scheduled restart', workerId);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (worker.isDead()) {
|
|
37
|
+
log_js_1.default.debug('Worker #%d is already dead before scheduled restart', worker.id);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return worker;
|
|
41
|
+
}
|
|
29
42
|
async function restartWorkers(delayBetweenIndividualWorkerRestarts, gracefulWorkerRestartTimeout) {
|
|
30
43
|
log_js_1.default.info('Started scheduled restart of workers');
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const workerIds = Object.keys(cluster_1.default.workers ?? {});
|
|
45
|
+
if (workerIds.length === 0) {
|
|
46
|
+
log_js_1.default.warn('No workers to restart');
|
|
47
|
+
return;
|
|
33
48
|
}
|
|
34
|
-
for (const
|
|
35
|
-
|
|
36
|
-
worker
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
log_js_1.default.
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
for (const workerId of workerIds) {
|
|
50
|
+
const worker = currentWorker(workerId);
|
|
51
|
+
if (worker) {
|
|
52
|
+
const workerToRestart = worker;
|
|
53
|
+
log_js_1.default.debug('Kill worker #%d', workerToRestart.id);
|
|
54
|
+
workerToRestart.isScheduledRestart = true;
|
|
55
|
+
// It's intended to restart worker in sequence, it shouldn't happen in parallel
|
|
56
|
+
// eslint-disable-next-line no-await-in-loop
|
|
57
|
+
await new Promise((resolve) => {
|
|
58
|
+
let timeout;
|
|
59
|
+
let isResolved = false;
|
|
60
|
+
let finish = () => { };
|
|
61
|
+
const onExit = () => {
|
|
62
|
+
finish();
|
|
63
|
+
};
|
|
64
|
+
const onError = (err) => {
|
|
65
|
+
log_js_1.default.warn({ msg: 'Error while waiting for scheduled worker restart', err });
|
|
66
|
+
if (!gracefulWorkerRestartTimeout) {
|
|
67
|
+
workerToRestart.isScheduledRestart = false;
|
|
68
|
+
finish();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const onSendError = (err) => {
|
|
72
|
+
if (!err || isResolved)
|
|
73
|
+
return;
|
|
74
|
+
workerToRestart.isScheduledRestart = false;
|
|
75
|
+
log_js_1.default.warn({ msg: 'Error sending scheduled graceful shutdown message to worker', err });
|
|
76
|
+
finish();
|
|
77
|
+
};
|
|
78
|
+
finish = () => {
|
|
79
|
+
if (isResolved)
|
|
80
|
+
return;
|
|
81
|
+
isResolved = true;
|
|
82
|
+
if (timeout)
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
workerToRestart.off('exit', onExit);
|
|
85
|
+
workerToRestart.off('error', onError);
|
|
53
86
|
resolve();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
};
|
|
88
|
+
workerToRestart.on('exit', onExit);
|
|
89
|
+
workerToRestart.on('error', onError);
|
|
90
|
+
try {
|
|
91
|
+
workerToRestart.send(utils_js_1.SHUTDOWN_WORKER_MESSAGE, onSendError);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
workerToRestart.isScheduledRestart = false;
|
|
95
|
+
log_js_1.default.warn({ msg: 'Error sending scheduled graceful shutdown message to worker', err });
|
|
96
|
+
finish();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (isResolved)
|
|
100
|
+
return;
|
|
101
|
+
// Zero means no timeout
|
|
102
|
+
if (gracefulWorkerRestartTimeout) {
|
|
103
|
+
timeout = setTimeout(() => {
|
|
104
|
+
log_js_1.default.debug('Worker #%d timed out, forcing kill it', workerToRestart.id);
|
|
105
|
+
workerToRestart.destroy();
|
|
106
|
+
finish();
|
|
107
|
+
}, gracefulWorkerRestartTimeout * MILLISECONDS_IN_SECOND);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// eslint-disable-next-line no-await-in-loop
|
|
111
|
+
await new Promise((resolve) => {
|
|
112
|
+
setTimeout(resolve, delayBetweenIndividualWorkerRestarts * MILLISECONDS_IN_MINUTE);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
61
115
|
}
|
|
62
116
|
log_js_1.default.info('Finished scheduled restart of workers');
|
|
63
117
|
}
|
package/lib/master.js
CHANGED
|
@@ -148,6 +148,7 @@ function masterRun(runningConfig) {
|
|
|
148
148
|
let isAbortingForStartupFailure = false;
|
|
149
149
|
let hasReportedStartupFailure = false;
|
|
150
150
|
let fatalStartupFailure = null;
|
|
151
|
+
const gracefulShutdownAcknowledgedWorkerIds = new Set();
|
|
151
152
|
// Set as soon as any shutdown path (external signal or startup-failure abort)
|
|
152
153
|
// begins. Read by the `cluster.on('exit')` handler to suppress re-forking
|
|
153
154
|
// workers that exit because we are intentionally tearing the cluster down.
|
|
@@ -163,13 +164,15 @@ function masterRun(runningConfig) {
|
|
|
163
164
|
// exits. A hard-deadline timer guarantees the master still exits if a worker
|
|
164
165
|
// is stuck.
|
|
165
166
|
const currentWorkers = () => Object.values(cluster_1.default.workers ?? {}).filter((worker) => Boolean(worker));
|
|
166
|
-
const forceKillSurvivingWorkers = (workers) => {
|
|
167
|
+
const forceKillSurvivingWorkers = (workers, { skipAcknowledgedWorkers = false } = {}) => {
|
|
167
168
|
workers.forEach((worker) => {
|
|
168
169
|
const workerProcess = worker.process;
|
|
169
170
|
// isDead() only: ChildProcess.killed means a signal was sent, not that
|
|
170
171
|
// the process died — e.g. a blocked worker surviving destroy()'s SIGTERM.
|
|
171
172
|
if (worker.isDead())
|
|
172
173
|
return;
|
|
174
|
+
if (skipAcknowledgedWorkers && gracefulShutdownAcknowledgedWorkerIds.has(worker.id))
|
|
175
|
+
return;
|
|
173
176
|
try {
|
|
174
177
|
workerProcess.kill('SIGKILL');
|
|
175
178
|
}
|
|
@@ -227,8 +230,13 @@ function masterRun(runningConfig) {
|
|
|
227
230
|
// Early force-kill of workers that cannot drain (blocked event loop).
|
|
228
231
|
// Their SIGKILL-induced exits complete the disconnect, which lets the
|
|
229
232
|
// normal waitForWorkerExits path below finish the shutdown promptly.
|
|
233
|
+
// ACK means the worker accepted shutdown and disconnected itself from new
|
|
234
|
+
// requests; it may still be draining an active render. Keep ACKed workers
|
|
235
|
+
// alive until the hard deadline so graceful drain can finish. Deployments
|
|
236
|
+
// with shorter supervisor grace windows can still terminate the master
|
|
237
|
+
// before that hard deadline and should tune the supervisor window.
|
|
230
238
|
const forceKillTimer = setTimeout(() => {
|
|
231
|
-
forceKillSurvivingWorkers(workersAtShutdown);
|
|
239
|
+
forceKillSurvivingWorkers(workersAtShutdown, { skipAcknowledgedWorkers: true });
|
|
232
240
|
}, SHUTDOWN_WORKER_FORCE_KILL_TIMEOUT_MS);
|
|
233
241
|
if (typeof forceKillTimer.unref === 'function')
|
|
234
242
|
forceKillTimer.unref();
|
|
@@ -274,6 +282,10 @@ function masterRun(runningConfig) {
|
|
|
274
282
|
return true;
|
|
275
283
|
};
|
|
276
284
|
cluster_1.default.on('message', (worker, message) => {
|
|
285
|
+
if (message === utils_js_1.SHUTDOWN_WORKER_ACK_MESSAGE) {
|
|
286
|
+
gracefulShutdownAcknowledgedWorkerIds.add(worker.id);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
277
289
|
// Check the abort flag first to short-circuit the type-guard on every
|
|
278
290
|
// ordinary IPC message once we are already aborting.
|
|
279
291
|
if (isAbortingForStartupFailure || !(0, workerMessages_js_1.isWorkerStartupFailureMessage)(message))
|
|
@@ -283,6 +295,7 @@ function masterRun(runningConfig) {
|
|
|
283
295
|
});
|
|
284
296
|
// Listen for dying workers:
|
|
285
297
|
cluster_1.default.on('exit', (worker) => {
|
|
298
|
+
gracefulShutdownAcknowledgedWorkerIds.delete(worker.id);
|
|
286
299
|
// Once a startup failure has been detected, abort regardless of whether
|
|
287
300
|
// this particular exit was from the failing worker, a scheduled restart,
|
|
288
301
|
// or an unrelated crash. Don't fork any more workers. If an external signal
|
|
@@ -336,7 +349,11 @@ function masterRun(runningConfig) {
|
|
|
336
349
|
log_js_1.default.info('Scheduled workers restarts every %d minutes (%d minutes btw each)', allWorkersRestartInterval, delayBetweenIndividualWorkerRestarts);
|
|
337
350
|
const allWorkersRestartIntervalMS = allWorkersRestartInterval * MILLISECONDS_IN_MINUTE;
|
|
338
351
|
const scheduleWorkersRestart = () => {
|
|
339
|
-
void (0, restartWorkers_js_1.default)(delayBetweenIndividualWorkerRestarts, gracefulWorkerRestartTimeout)
|
|
352
|
+
void (0, restartWorkers_js_1.default)(delayBetweenIndividualWorkerRestarts, gracefulWorkerRestartTimeout)
|
|
353
|
+
.catch((err) => {
|
|
354
|
+
log_js_1.default.error({ msg: 'Scheduled worker restart failed', err });
|
|
355
|
+
})
|
|
356
|
+
.finally(() => {
|
|
340
357
|
setTimeout(scheduleWorkersRestart, allWorkersRestartIntervalMS);
|
|
341
358
|
});
|
|
342
359
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export type RscPeerCheckLevel = 'ok' | 'warn' | 'error';
|
|
2
1
|
export type RscPeerCheckResult = {
|
|
3
2
|
level: 'ok';
|
|
4
3
|
message?: undefined;
|
|
5
4
|
} | {
|
|
6
|
-
level:
|
|
5
|
+
level: 'error';
|
|
7
6
|
message: string;
|
|
8
7
|
};
|
|
9
8
|
export interface RscPeerCheckInput {
|
|
@@ -16,35 +16,83 @@
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.checkRscPeerCompatibility = checkRscPeerCompatibility;
|
|
18
18
|
const rscPeerSupport_js_1 = require("./rscPeerSupport.js");
|
|
19
|
-
|
|
20
|
-
// `19.0.5-rc.7` compares as `19.0.5`. We only need major/minor/patch ordering, so this
|
|
21
|
-
// avoids semver's prerelease rules (and a `semver` dependency) entirely.
|
|
22
|
-
const parseTuple = (version) => {
|
|
19
|
+
const parseVersion = (version) => {
|
|
23
20
|
// `resolveVersion` is a public injection point, so tolerate a leading `v`/`=` (e.g. `v19.0.4`).
|
|
24
21
|
// Malformed versions intentionally coerce to 0 segments so the major mismatch
|
|
25
22
|
// branch reports the original string instead of hiding it behind a parse error.
|
|
26
23
|
const normalized = version.replace(/^[v=]+/, '');
|
|
27
|
-
const
|
|
28
|
-
const
|
|
24
|
+
const buildIndex = normalized.indexOf('+');
|
|
25
|
+
const withoutBuild = buildIndex === -1 ? normalized : normalized.slice(0, buildIndex);
|
|
26
|
+
const prereleaseIndex = withoutBuild.indexOf('-');
|
|
27
|
+
const core = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex);
|
|
28
|
+
const prerelease = prereleaseIndex === -1 ? null : withoutBuild.slice(prereleaseIndex + 1) || null;
|
|
29
29
|
const parts = core.split('.');
|
|
30
|
-
return
|
|
30
|
+
return {
|
|
31
|
+
tuple: [Number(parts[0]) || 0, Number(parts[1]) || 0, Number(parts[2]) || 0],
|
|
32
|
+
prerelease: prerelease || null,
|
|
33
|
+
};
|
|
31
34
|
};
|
|
32
|
-
const
|
|
35
|
+
const parseTuple = (version) => parseVersion(version).tuple;
|
|
36
|
+
const comparePrereleasePart = (left, right) => {
|
|
37
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
38
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
39
|
+
if (leftNumeric && rightNumeric)
|
|
40
|
+
return Number(left) - Number(right);
|
|
41
|
+
if (leftNumeric)
|
|
42
|
+
return -1;
|
|
43
|
+
if (rightNumeric)
|
|
44
|
+
return 1;
|
|
45
|
+
if (left < right)
|
|
46
|
+
return -1;
|
|
47
|
+
if (left > right)
|
|
48
|
+
return 1;
|
|
49
|
+
return 0;
|
|
50
|
+
};
|
|
51
|
+
const comparePrerelease = (left, right) => {
|
|
52
|
+
if (left === right)
|
|
53
|
+
return 0;
|
|
54
|
+
if (!left)
|
|
55
|
+
return 1;
|
|
56
|
+
if (!right)
|
|
57
|
+
return -1;
|
|
58
|
+
const leftParts = left.split('.');
|
|
59
|
+
const rightParts = right.split('.');
|
|
60
|
+
const maxParts = Math.max(leftParts.length, rightParts.length);
|
|
61
|
+
for (let i = 0; i < maxParts; i += 1) {
|
|
62
|
+
const leftPart = leftParts[i];
|
|
63
|
+
const rightPart = rightParts[i];
|
|
64
|
+
if (leftPart === undefined)
|
|
65
|
+
return -1;
|
|
66
|
+
if (rightPart === undefined)
|
|
67
|
+
return 1;
|
|
68
|
+
const comparison = comparePrereleasePart(leftPart, rightPart);
|
|
69
|
+
if (comparison !== 0)
|
|
70
|
+
return comparison;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
};
|
|
74
|
+
const compareVersions = (actual, floor) => {
|
|
75
|
+
const actualVersion = parseVersion(actual);
|
|
76
|
+
const floorVersion = parseVersion(floor);
|
|
33
77
|
for (let i = 0; i < 3; i += 1) {
|
|
34
|
-
const a =
|
|
35
|
-
const f =
|
|
78
|
+
const a = actualVersion.tuple[i] ?? 0;
|
|
79
|
+
const f = floorVersion.tuple[i] ?? 0;
|
|
36
80
|
if (a > f)
|
|
37
|
-
return
|
|
81
|
+
return 1;
|
|
38
82
|
if (a < f)
|
|
39
|
-
return
|
|
83
|
+
return -1;
|
|
40
84
|
}
|
|
41
|
-
return
|
|
85
|
+
return comparePrerelease(actualVersion.prerelease, floorVersion.prerelease);
|
|
42
86
|
};
|
|
87
|
+
const isAtLeastVersion = (actual, floor) => compareVersions(actual, floor) >= 0;
|
|
43
88
|
const sameTuple = (left, right) => left.every((value, index) => value === right[index]);
|
|
44
89
|
const supportedRscRange = ({ supportedMajor }, { supportedRanges }) => {
|
|
45
90
|
const supportedMinors = [...new Set(supportedRanges.map((range) => range.rscMinor))].sort((left, right) => left - right);
|
|
46
91
|
return supportedMinors.map((minor) => `${supportedMajor}.${minor}.x`).join(' or ');
|
|
47
92
|
};
|
|
93
|
+
const rscFloorRange = ({ minimumVersion, minimumPrereleaseVersion, }) => minimumPrereleaseVersion
|
|
94
|
+
? `>= ${minimumVersion} (or ${minimumPrereleaseVersion} during the RC soak)`
|
|
95
|
+
: `>= ${minimumVersion}`;
|
|
48
96
|
const supportedReactRange = (rscTuple, { supportedMajor, supportedRanges }) => {
|
|
49
97
|
const rscMinor = rscTuple[1];
|
|
50
98
|
const matchingRanges = supportedRanges.filter((range) => range.rscMinor === rscMinor);
|
|
@@ -62,11 +110,6 @@ const errorMessage = (pkg, found, want, proVersion) => [
|
|
|
62
110
|
` Upgrade or downgrade ${pkg} to a compatible release. See https://www.shakacode.com/react-on-rails-pro/docs/.`,
|
|
63
111
|
` (Set REACT_ON_RAILS_PRO_DISABLE_VERSION_CHECK=1 to downgrade this error to a warning.)`,
|
|
64
112
|
].join('\n');
|
|
65
|
-
const warnMessage = (found, recommendedMin, proVersion) => [
|
|
66
|
-
`[ReactOnRails] react-on-rails-rsc ${found} is older than the recommended minimum ${recommendedMin}.`,
|
|
67
|
-
` ${proLabel(proVersion)} may behave incorrectly (missing coordinated RSC fixes).`,
|
|
68
|
-
` Upgrade react-on-rails-rsc to ${recommendedMin} or newer.`,
|
|
69
|
-
].join('\n');
|
|
70
113
|
function checkRscPeerCompatibility(input) {
|
|
71
114
|
const { rscVersion, reactVersion, reactDomVersion, proVersion } = input;
|
|
72
115
|
// react-on-rails-rsc is an optional peer. Absent => the consumer is not on the RSC
|
|
@@ -74,7 +117,8 @@ function checkRscPeerCompatibility(input) {
|
|
|
74
117
|
if (!rscVersion)
|
|
75
118
|
return { level: 'ok' };
|
|
76
119
|
const { reactOnRailsRsc, react } = rscPeerSupport_js_1.RSC_PEER_SUPPORT;
|
|
77
|
-
const
|
|
120
|
+
const rscParsedVersion = parseVersion(rscVersion);
|
|
121
|
+
const rscTuple = rscParsedVersion.tuple;
|
|
78
122
|
const [rscMajor] = rscTuple;
|
|
79
123
|
if (rscMajor !== reactOnRailsRsc.supportedMajor) {
|
|
80
124
|
return {
|
|
@@ -82,6 +126,20 @@ function checkRscPeerCompatibility(input) {
|
|
|
82
126
|
message: errorMessage('react-on-rails-rsc', rscVersion, `${reactOnRailsRsc.supportedMajor}.x`, proVersion),
|
|
83
127
|
};
|
|
84
128
|
}
|
|
129
|
+
const meetsStableFloor = !rscParsedVersion.prerelease && isAtLeastVersion(rscVersion, reactOnRailsRsc.minimumVersion);
|
|
130
|
+
const minimumPrereleaseTuple = reactOnRailsRsc.minimumPrereleaseVersion
|
|
131
|
+
? parseTuple(reactOnRailsRsc.minimumPrereleaseVersion)
|
|
132
|
+
: null;
|
|
133
|
+
const meetsPrereleaseFloor = reactOnRailsRsc.minimumPrereleaseVersion &&
|
|
134
|
+
minimumPrereleaseTuple &&
|
|
135
|
+
sameTuple(rscTuple, minimumPrereleaseTuple) &&
|
|
136
|
+
isAtLeastVersion(rscVersion, reactOnRailsRsc.minimumPrereleaseVersion);
|
|
137
|
+
if (!meetsStableFloor && !meetsPrereleaseFloor) {
|
|
138
|
+
return {
|
|
139
|
+
level: 'error',
|
|
140
|
+
message: errorMessage('react-on-rails-rsc', rscVersion, rscFloorRange(reactOnRailsRsc), proVersion),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
85
143
|
if (!isSupportedRscMinor(rscTuple, react)) {
|
|
86
144
|
return {
|
|
87
145
|
level: 'error',
|
|
@@ -115,9 +173,6 @@ function checkRscPeerCompatibility(input) {
|
|
|
115
173
|
};
|
|
116
174
|
}
|
|
117
175
|
}
|
|
118
|
-
if (!isAtLeast(rscTuple, parseTuple(reactOnRailsRsc.recommendedMin))) {
|
|
119
|
-
return { level: 'warn', message: warnMessage(rscVersion, reactOnRailsRsc.recommendedMin, proVersion) };
|
|
120
|
-
}
|
|
121
176
|
return { level: 'ok' };
|
|
122
177
|
}
|
|
123
178
|
//# sourceMappingURL=checkRscPeerCompatibility.js.map
|
|
@@ -92,13 +92,20 @@ function normalizedRuntimeEnvs() {
|
|
|
92
92
|
.filter((value) => Boolean(value))
|
|
93
93
|
.map((value) => value.toLowerCase());
|
|
94
94
|
}
|
|
95
|
-
function runtimeEnvsAllowDevelopmentDefaults() {
|
|
96
|
-
const runtimeEnvs = normalizedRuntimeEnvs();
|
|
95
|
+
function runtimeEnvsAllowDevelopmentDefaults(runtimeEnvs = normalizedRuntimeEnvs()) {
|
|
97
96
|
// Fail closed: every present runtime env must be development/test before we allow
|
|
98
97
|
// missing-password defaults. Any production-like value, or no env at all, still
|
|
99
98
|
// requires an explicit password.
|
|
100
99
|
return runtimeEnvs.length > 0 && runtimeEnvs.every((value) => value === 'development' || value === 'test');
|
|
101
100
|
}
|
|
101
|
+
function unsetRuntimeEnvPasswordGuidance(runtimeEnvs) {
|
|
102
|
+
if (runtimeEnvs.length > 0) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
return ('\n\nBoth RAILS_ENV and NODE_ENV are unset. For a local Rails development shell, either set them explicitly:\n\n' +
|
|
106
|
+
' export RAILS_ENV=development NODE_ENV=development\n\n' +
|
|
107
|
+
'or configure RENDERER_PASSWORD. Deployed/shared environments should set explicit envs and RENDERER_PASSWORD.');
|
|
108
|
+
}
|
|
102
109
|
// Intentionally checks only NODE_ENV, not both NODE_ENV and RAILS_ENV like
|
|
103
110
|
// runtimeEnvsAllowDevelopmentDefaults(). Async operation log replay is a JS
|
|
104
111
|
// debugging concern, not a security boundary — it should key off the JS
|
|
@@ -204,24 +211,25 @@ function logSanitizedConfig() {
|
|
|
204
211
|
const KNOWN_WEAK_PASSWORDS = new Set(['devPassword', 'myPassword1', 'password', 'changeme', 'admin', 'secret', 'test', 'renderer'].map((p) => p.toLowerCase()));
|
|
205
212
|
const MIN_PASSWORD_LENGTH = 16;
|
|
206
213
|
function validatePasswordForProduction(aConfig) {
|
|
207
|
-
const
|
|
214
|
+
const runtimeEnvs = normalizedRuntimeEnvs();
|
|
215
|
+
const isProductionLike = !runtimeEnvsAllowDevelopmentDefaults(runtimeEnvs);
|
|
208
216
|
if (!aConfig.password || aConfig.password.trim() === '') {
|
|
209
217
|
if (isProductionLike) {
|
|
210
|
-
return (
|
|
218
|
+
return (`RENDERER_PASSWORD must be set in production-like environments ` +
|
|
211
219
|
`(NODE_ENV: "${env.NODE_ENV ?? '(not set)'}", RAILS_ENV: "${env.RAILS_ENV ?? '(not set)'}").` +
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
`\n\n` +
|
|
221
|
+
`In development and test environments, the renderer password is optional and no authentication\n` +
|
|
222
|
+
`is required. In all other environments, you must explicitly configure a password to secure\n` +
|
|
223
|
+
`communication between Rails and the Node Renderer.${unsetRuntimeEnvPasswordGuidance(runtimeEnvs)}\n\n` +
|
|
224
|
+
`To secure the renderer, set the RENDERER_PASSWORD environment variable:\n\n` +
|
|
225
|
+
` export RENDERER_PASSWORD="your-secure-password"\n\n` +
|
|
226
|
+
`Or pass it in the config object:\n\n` +
|
|
227
|
+
` reactOnRailsProNodeRenderer({ password: process.env.RENDERER_PASSWORD });\n\n` +
|
|
228
|
+
`Environment matrix:\n` +
|
|
229
|
+
` development — password optional (no authentication)\n` +
|
|
230
|
+
` test — password optional (no authentication)\n` +
|
|
231
|
+
` (both unset) — treated as production-like; RENDERER_PASSWORD required\n` +
|
|
232
|
+
` all other environments (staging, production, qa, preview, etc.) — RENDERER_PASSWORD required`);
|
|
225
233
|
}
|
|
226
234
|
return null;
|
|
227
235
|
}
|
|
@@ -57,6 +57,7 @@ const licensePublicKey_js_1 = require("./licensePublicKey.js");
|
|
|
57
57
|
* Valid license plan types.
|
|
58
58
|
* Must match VALID_PLANS in react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
|
|
59
59
|
*/
|
|
60
|
+
// MIRROR VALUES OF: react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
|
|
60
61
|
const VALID_PLANS = ['paid', 'startup', 'nonprofit', 'education', 'oss', 'partner'];
|
|
61
62
|
// Module-level state for caching license validation results.
|
|
62
63
|
//
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
export declare const RSC_PEER_SUPPORT: {
|
|
2
2
|
readonly reactOnRailsRsc: {
|
|
3
|
-
readonly
|
|
3
|
+
readonly minimumVersion: "19.2.1";
|
|
4
|
+
readonly minimumPrereleaseVersion: "19.2.1-rc.0";
|
|
4
5
|
readonly supportedMajor: 19;
|
|
5
6
|
};
|
|
6
7
|
readonly react: {
|
|
7
8
|
readonly supportedMajor: 19;
|
|
8
9
|
readonly supportedRanges: readonly [{
|
|
9
|
-
readonly rscMinor: 0;
|
|
10
|
-
readonly minor: 0;
|
|
11
|
-
readonly minPatch: 4;
|
|
12
|
-
}, {
|
|
13
10
|
readonly rscMinor: 2;
|
|
14
11
|
readonly minor: 2;
|
|
15
12
|
readonly minPatch: 7;
|
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.RSC_PEER_SUPPORT = void 0;
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// Node-renderer source of truth for react-on-rails-pro's RSC peer compatibility window.
|
|
19
|
+
// Ruby Doctor mirrors these values and has a parity spec to catch cross-language drift.
|
|
20
20
|
//
|
|
21
|
-
// `
|
|
22
|
-
//
|
|
23
|
-
// 19.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
21
|
+
// `minimumVersion` is the React on Rails 17 RSC floor. Keep it in sync with the
|
|
22
|
+
// Ruby Doctor/generator constants that install and diagnose the same Pro RSC
|
|
23
|
+
// package line. The 19.2.1 line pairs with React/React DOM 19.2.7 and carries
|
|
24
|
+
// the coordinated RSC fixes required by the Pro RSC renderer path.
|
|
25
|
+
// `minimumPrereleaseVersion` keeps the 17.0 RC soak installable until the stable
|
|
26
|
+
// 19.2.1 package is published without accepting older prereleases on the same
|
|
27
|
+
// tuple. When it is set, keep its core tuple equal to `minimumVersion`.
|
|
27
28
|
exports.RSC_PEER_SUPPORT = {
|
|
28
|
-
reactOnRailsRsc: {
|
|
29
|
+
reactOnRailsRsc: { minimumVersion: '19.2.1', minimumPrereleaseVersion: '19.2.1-rc.0', supportedMajor: 19 },
|
|
29
30
|
react: {
|
|
30
31
|
supportedMajor: 19,
|
|
31
32
|
supportedRanges: [
|
|
32
|
-
{ rscMinor: 0, minor: 0, minPatch: 4 },
|
|
33
33
|
// React 19.2.7 is the coordinated floor for react-on-rails-rsc 19.2.x.
|
|
34
34
|
{ rscMinor: 2, minor: 2, minPatch: 7 },
|
|
35
35
|
],
|
|
@@ -106,11 +106,6 @@ function runRscPeerCompatibilityCheck(options = {}) {
|
|
|
106
106
|
});
|
|
107
107
|
if (result.level === 'ok')
|
|
108
108
|
return;
|
|
109
|
-
if (result.level === 'warn') {
|
|
110
|
-
// DISABLE_ENV only downgrades hard errors; warnings still fire so operators see the degraded-version signal.
|
|
111
|
-
log_js_1.default.warn(result.message);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
109
|
if (env[DISABLE_ENV] === '1') {
|
|
115
110
|
log_js_1.default.warn(`${result.message}\n(Version check downgraded to a warning via ${DISABLE_ENV}.)`);
|
|
116
111
|
return;
|
|
@@ -41,6 +41,9 @@ function replayConsoleOnRenderer(consoleHistory) {
|
|
|
41
41
|
// AsyncLocalStorage is available in Node.js 12.17.0 and later versions
|
|
42
42
|
const canUseAsyncLocalStorage = () => typeof async_hooks_1.AsyncLocalStorage !== 'undefined' && (0, configBuilder_js_1.getConfig)().replayServerAsyncOperationLogs;
|
|
43
43
|
class SharedConsoleHistory {
|
|
44
|
+
asyncLocalStorageIfEnabled;
|
|
45
|
+
isRunningSyncOperation;
|
|
46
|
+
syncHistory;
|
|
44
47
|
constructor() {
|
|
45
48
|
if (canUseAsyncLocalStorage()) {
|
|
46
49
|
this.asyncLocalStorageIfEnabled = new async_hooks_1.AsyncLocalStorage();
|
package/lib/shared/utils.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { RenderResult } from '../worker/vm.js';
|
|
|
6
6
|
import { remapStackTrace } from '../worker/vmSourceMapSupport.js';
|
|
7
7
|
export declare const TRUNCATION_FILLER = "\n... TRUNCATED ...\n";
|
|
8
8
|
export declare const SHUTDOWN_WORKER_MESSAGE = "NODE_RENDERER_SHUTDOWN_WORKER";
|
|
9
|
+
export declare const SHUTDOWN_WORKER_ACK_MESSAGE = "NODE_RENDERER_SHUTDOWN_WORKER_ACK";
|
|
9
10
|
export declare function workerIdLabel(): number | "NO WORKER ID";
|
|
10
11
|
export declare function smartTrim(value: unknown, maxLength?: number): string | null;
|
|
11
12
|
export interface ResponseResult {
|
|
@@ -35,6 +36,7 @@ export type RequestInfo = {
|
|
|
35
36
|
* registration-scoped remapper to avoid rewriting unrelated bundle paths.
|
|
36
37
|
*/
|
|
37
38
|
export declare function formatExceptionMessage(request: RequestInfo, error: unknown, context?: string, stackRemapper?: typeof remapStackTrace): string;
|
|
39
|
+
export declare function validateAssetFilename(filename: unknown): string;
|
|
38
40
|
export declare function saveMultipartFile(multipartFile: MultipartFile, destinationPath: string): Promise<void>;
|
|
39
41
|
export interface Asset {
|
|
40
42
|
type: 'asset';
|